openapi: 3.0.3
info:
  title: Repzo API - AI Object Detection Metric
  version: 1.0.0
  description: |
    **Metrics** are schema-strict planogram checks — the business layer on top
    of Object Detection session analyses. A metric behaves like a typed
    function: its `type` picks an implementation from the engine registry and
    its `args` are validated against that type's declared argument schema
    (required/optional fields, bounds). There are NO user formulas, flow
    charts or scripts — every use case is a dedicated, well-tested type, and
    adding one is a registry entry. Persistence is equally strict: `args` is a
    Mongoose embedded DISCRIMINATOR keyed by the metric type (the promotions
    model pattern), so unknown keys never reach the database.

    **Output families.** Every type belongs to one family, which fixes what
    the result's `answer` means and where its `score` 0..1 comes from:
    - `compatibility` — `answer` is a boolean verdict; score = answer ? 1 : 0.
      Mission points = score × the mission's weight.
    - `numerical` — `answer` is a collected VALUE (data capture). Types may
      accept an optional `target_answer`: score = answer ÷ target_answer
      (clamped to 1). Without a target (and no evaluator-supplied factor) a
      NON-ZERO answer earns full score (1); a missing or zero answer scores
      0.
    - `share_of_shelf` — `answer` is the MAIN SEGMENT's measured quantity
      (Σ cm of facing widths, Σ cm² of front-face area, or facing count);
      `total` is the CONSIDERED CATEGORY — the same measure over facings
      whose label belongs to ANY of the metric's segments (facings outside
      every segment never dilute the share); `ratio` = answer / total;
      `target_answer` = target_ratio × total; score =
      min(1, ratio / target_ratio). Every segment row is reported as a
      SegmentOutput in `computed.segments[]`; only the main row carries the
      target and a score.

    **Types.**
    - `adjacent_block` (compatibility) — "these labels must stand together":
      the stacks holding ANY of `labels` must form contiguous blocks (no
      outside label cutting between them on a shelf) and the total facing
      count must lie in `[from, to]`.
    - `facings_count` (numerical) — data collection: how many facings of the
      given labels does the shopper see (front row unless disabled)? An
      optional `target_answer` ("I demand ≥ N facings") turns it into a
      scored check: score = counted ÷ target, capped at 1.
    - `on_shelf_availability` (numerical) — OSA: how much of what SHOULD be
      on the shelf actually is? A selected label is AVAILABLE when ≥ 1
      facing of it stands anywhere on a shelf (any depth row by default —
      `front_row_only: true` narrows to the front). `answer` = available
      count and `total` = selected count. An optional `target_answer` ("at
      least N of these available", never above the label count) switches
      scoring to answer ÷ target; without it the evaluator returns its own
      factor `ratio` = answer ÷ total, so mission points = weight ×
      availability. `missing` lists the out-of-stock labels with names.
    - `share_of_shelf` (share_of_shelf) — references SEGMENTS
      (`/ai-object-detection-segment`, reusable named label sets); each row
      references exactly ONE segment (and a segment may appear in only one
      row) and may override its labels for this metric only (empty override
      = the segment's own). Exactly ONE row is the MAIN segment (`main:
      true`; the first row when none is flagged) — `target_ratio` is the
      goal FOR THAT SEGMENT; the other rows are context (competitors, the
      category) measured for comparison, never scored. `measure` picks
      linear width (`width_cm`), front-face area (`area_cm2`) or facing
      count (`facings` — no physical sizes needed).

    **Evaluation.** Every ENABLED metric a MISSION assigns to the session is
    evaluated automatically against each successful session analysis (and
    re-evaluated after a compose-only shelf recompute), producing one
    `ai-object-detection-metric-result` per (analysis, metric).

    **Registry introspection.** `GET ?registry=true` returns the type
    declarations (args schema, output family, overwritable keys) so generic
    clients can render forms without hardcoding.

    **Multi-tenancy & lifecycle.** Scoped by `company_namespace` (injected
    from the caller's token; SDK callers may pass an explicit override on
    create), soft-deleted via `disabled: true`. `enabled` pauses evaluation
    without losing the definition. Args are re-validated on every write
    (`PUT` is a full re-definition — `type` and `args` are required);
    invalid definitions are rejected with the full violation list;
    share-of-shelf rows must reference LIVE segments. `PATCH` is not
    supported (400 — use `PUT`). Admin-facing.
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /ai-object-detection-metric:
    get:
      summary: List metrics (or the type registry)
      operationId: findAiObjectDetectionMetric
      parameters:
        - in: query
          name: registry
          description: "`true` returns the metric TYPE declarations (args schema, output family, overwritable keys) instead of documents."
          schema: { type: boolean }
        - in: query
          name: _id
          description: "Filter by `_id`. Pass once or as `?_id[]=...` for multiple."
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: name
          description: Filter by exact name.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: search
          description: Case-insensitive substring search on `name`.
          schema: { type: string }
        - in: query
          name: type
          description: Filter by metric type (registry key).
          schema:
            type: string
            enum:
              [
                adjacent_block,
                facings_count,
                on_shelf_availability,
                share_of_shelf,
              ]
        - in: query
          name: enabled
          schema: { type: boolean }
        - in: query
          name: disabled
          description: "Include soft-deleted metrics (`true`) or only live ones (`false`). Omitted = no filter on the flag."
          schema: { type: boolean }
        - in: query
          name: from_updatedAt
          description: Lower bound on `updatedAt` (Unix ms).
          schema: { type: number }
        - in: query
          name: to_updatedAt
          description: Upper bound on `updatedAt` (Unix ms).
          schema: { type: number }
        - in: query
          name: from_createdAt
          description: Lower bound on `createdAt` (Unix ms).
          schema: { type: number }
        - in: query
          name: to_createdAt
          description: Upper bound on `createdAt` (Unix ms).
          schema: { type: number }
        - in: query
          name: per_page
          description: Page size. Defaults to the server's configured pagination limit.
          schema: { type: integer, minimum: 1 }
        - in: query
          name: page
          description: 1-indexed page number.
          schema: { type: integer, minimum: 1 }
      responses:
        "200":
          description: "Paginated metric documents (newest `_id` first), or the registry when `registry=true`."
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/MetricFindResult"
                  - $ref: "#/components/schemas/MetricRegistry"
    post:
      summary: Create a metric
      operationId: createAiObjectDetectionMetric
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MetricCreateBody"
      responses:
        "201":
          description: The created metric.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Metric"
        "400":
          description: "Validation failed — the message lists EVERY args violation (e.g. `segments needs at least 1 segment(s); target_ratio must be ≤ 1`), or names unknown/deleted segments."
  /ai-object-detection-metric/{id}:
    get:
      summary: Get a metric
      operationId: getAiObjectDetectionMetric
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The metric document.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Metric"
        "400":
          description: No metric with that id in the caller's namespace.
    put:
      summary: Update a metric (args re-validated)
      description: |
        A full re-definition: `type` and `args` are REQUIRED and re-validated
        (the stored args discriminator is re-cast to the new type). `name`,
        `description`, `enabled` and `disabled` are optional. `_id`,
        `company_namespace` and `creator` in the body are ignored; `editor`
        is stamped from the token.
      operationId: updateAiObjectDetectionMetric
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MetricUpdateBody"
      responses:
        "200":
          description: The updated metric.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Metric"
        "400":
          description: Validation failed — every violation listed.
        "404":
          description: Metric not found.
    delete:
      summary: Soft-delete a metric
      description: "Sets `disabled: true` and stamps `editor`. Existing results keep their snapshots."
      operationId: removeAiObjectDetectionMetric
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The disabled metric.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Metric"
        "404":
          description: Metric not found.
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: api-key
    JwtAuth:
      type: apiKey
      in: header
      name: Authorization
  schemas:
    AdjacentBlockArgs:
      type: object
      description: "Args for `type: adjacent_block` (compatibility family)."
      required: [labels, from, to]
      properties:
        labels:
          type: array
          minItems: 1
          items: { type: string }
          description: Label ids that form the block.
        from:
          type: integer
          minimum: 0
          description: Minimum member facing count (inclusive).
        to:
          type: integer
          minimum: 0
          description: "Maximum member facing count (inclusive, from ≤ to)."
        front_row_only:
          type: boolean
          default: true
          description: Judge only the shopper-visible front row.
    FacingsCountArgs:
      type: object
      description: "Args for `type: facings_count` (numerical family)."
      required: [labels]
      properties:
        labels:
          type: array
          minItems: 1
          items: { type: string }
          description: Label ids to count.
        target_answer:
          type: integer
          minimum: 1
          description: "Optional demanded count — score = answer ÷ target_answer (clamped to 1). Absent: a non-zero count earns full score, zero earns 0."
        front_row_only:
          type: boolean
          default: true
          description: Count only the shopper-visible front row.
    OnShelfAvailabilityArgs:
      type: object
      description: "Args for `type: on_shelf_availability` (numerical family)."
      required: [labels]
      properties:
        labels:
          type: array
          minItems: 1
          items: { type: string }
          description: The label ids that SHOULD be on the shelf.
        target_answer:
          type: integer
          minimum: 1
          description: "Optional demanded count of AVAILABLE labels — score = answer ÷ target_answer (clamped to 1). Absent: score is the availability ratio (answer ÷ total). Rejected when it exceeds the number of labels selected, since that target could never be met."
        front_row_only:
          type: boolean
          default: false
          description: "Default false — a product in a back row is still available. true narrows availability to the shopper-visible front row."
    ShareOfShelfArgs:
      type: object
      description: "Args for `type: share_of_shelf` (share family)."
      required: [segments, target_ratio]
      properties:
        segments:
          type: array
          minItems: 1
          description: |
            Segment rows. Each row references exactly ONE segment
            (`/ai-object-detection-segment`, a single ObjectId — never an
            array) and a segment may appear in only one row (duplicates are
            rejected: the same segment twice would double-count its share in
            segment-level reporting). A row may override its segment's labels
            FOR THIS METRIC ONLY — an empty/absent override means the
            segment's own labels apply (and follow later segment edits
            automatically).
            Exactly ONE row must be the MAIN segment (`main: true`) — the one
            `target_ratio` is defined for. Flagging none makes the FIRST row
            main; flagging more than one is rejected. The other rows are
            context (competitors, the category): measured and reported, never
            scored.
          items:
            type: object
            required: [segment]
            properties:
              segment:
                type: string
                description: Segment id (must exist and not be deleted).
              labels:
                type: array
                items: { type: string }
                description: Optional per-metric label override.
              main:
                type: boolean
                default: false
                description: "The row the metric's target is defined for — exactly one per metric."
        target_ratio:
          type: number
          minimum: 0.001
          maximum: 1
          description: "The share the MAIN segment must reach — score = min(1, main_ratio / target_ratio)."
        measure:
          type: string
          enum: [width_cm, area_cm2, facings]
          default: width_cm
          description: "width_cm = occupied shelf length (linear share; see first_in_stack), area_cm2 = front-face areas, facings = unit count."
        front_row_only:
          type: boolean
          default: true
          description: Measure only the shopper-visible front row.
        first_in_stack:
          type: boolean
          default: true
          description: "width_cm only: count each vertical pile ONCE — the BOTTOM (first-in-stack) object books the shelf distance, so stacked units don't inflate the linear share beyond the occupied shelf length. false = the legacy per-unit width sum. Ignored by area_cm2 / facings."
    MetricArgs:
      description: |
        Arguments matching the type's schema — validated and NORMALIZED
        server-side (unknown keys dropped, defaults applied, every
        violation listed). The server stamps `args.type` internally for
        the persistence discriminator; clients never send it.
      oneOf:
        - $ref: "#/components/schemas/AdjacentBlockArgs"
        - $ref: "#/components/schemas/FacingsCountArgs"
        - $ref: "#/components/schemas/OnShelfAvailabilityArgs"
        - $ref: "#/components/schemas/ShareOfShelfArgs"
    MetricCreateBody:
      type: object
      description: |
        Body for creating a metric. The tenant key (`company_namespace`) is
        optional for SDK callers and is otherwise injected from the caller's
        session.
      required: [name, type, args]
      properties:
        name: { type: string }
        description: { type: string }
        type:
          type: string
          description: Engine registry key — selects the args schema.
          enum:
            [
              adjacent_block,
              facings_count,
              on_shelf_availability,
              share_of_shelf,
            ]
        args:
          $ref: "#/components/schemas/MetricArgs"
        enabled:
          type: boolean
          default: true
          description: OFF pauses evaluation without losing the definition.
        company_namespace:
          type: array
          items: { type: string }
          description: Optional tenant namespace override for SDK callers.
    MetricUpdateBody:
      type: object
      description: |
        Body for `PUT` — a full re-definition. `type` and `args` are required
        and re-validated; the other fields are optional. Set `disabled: true`
        to soft-delete. `company_namespace` is derived from the session — do
        not send it.
      required: [type, args]
      properties:
        name: { type: string }
        description: { type: string }
        type:
          type: string
          enum:
            [
              adjacent_block,
              facings_count,
              on_shelf_availability,
              share_of_shelf,
            ]
        args:
          $ref: "#/components/schemas/MetricArgs"
        enabled: { type: boolean }
        disabled:
          type: boolean
          description: Soft-delete flag.
    Metric:
      type: object
      properties:
        _id: { type: string }
        disabled: { type: boolean }
        name: { type: string }
        description: { type: string }
        type:
          type: string
          enum:
            [
              adjacent_block,
              facings_count,
              on_shelf_availability,
              share_of_shelf,
            ]
        args:
          description: "Stored args (strict per-type shape; includes the internal discriminator mirror `args.type`)."
          oneOf:
            - $ref: "#/components/schemas/AdjacentBlockArgs"
            - $ref: "#/components/schemas/FacingsCountArgs"
            - $ref: "#/components/schemas/OnShelfAvailabilityArgs"
            - $ref: "#/components/schemas/ShareOfShelfArgs"
        enabled: { type: boolean }
        creator:
          $ref: "#/components/schemas/UserStamp"
        editor:
          $ref: "#/components/schemas/UserStamp"
        company_namespace:
          type: array
          items: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    UserStamp:
      type: object
      description: "Who created / last edited the document (server-stamped from the token)."
      properties:
        _id: { type: string }
        name: { type: string }
        type:
          type: string
          enum: [admin, rep, client, tenant]
        admin: { type: string }
        rep: { type: string }
        client: { type: string }
        tenant: { type: string }
    MetricArgField:
      type: object
      description: One argument declaration of a metric type (drives generic forms and server validation).
      properties:
        key: { type: string }
        type:
          type: string
          enum: [labels, number, integer, boolean, enum, segments]
        required: { type: boolean }
        min: { type: number }
        max: { type: number }
        default:
          description: Default applied when an optional arg is absent.
        min_items:
          type: integer
          description: "labels / segments: minimum item count."
        options:
          type: array
          items: { type: string }
          description: "enum: the allowed values."
    MetricRegistry:
      type: object
      description: "Response of `GET ?registry=true` — the metric type declarations."
      properties:
        engine_version:
          type: integer
          description: Bumped whenever a type's semantics change; results carry the version they were computed with.
        types:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
                enum:
                  [
                    adjacent_block,
                    facings_count,
                    on_shelf_availability,
                    share_of_shelf,
                  ]
              output:
                type: string
                enum: [compatibility, numerical, share_of_shelf]
              args:
                type: array
                items:
                  $ref: "#/components/schemas/MetricArgField"
              overwritable:
                type: array
                items: { type: string }
                description: "Result output keys a human may override (`score` / `ratio` are always derived)."
    MetricFindResult:
      type: object
      description: Standard paginated result envelope.
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Metric"
        total_result: { type: number }
        current_count: { type: number }
        total_pages: { type: number }
        current_page: { type: number }
        per_page: { type: number }
        first_page_url: { type: string }
        last_page_url: { type: string }
        next_page_url: { type: string, nullable: true }
        prev_page_url: { type: string, nullable: true }
        path: { type: string }
