openapi: 3.0.3
info:
  title: Repzo API - AI Object Detection Session Insight
  version: 1.0.0
  description: |
    **Session insight** — retail shelf intelligence computed ON READ from a
    shelf-scan session's latest successful analysis (or an explicit analysis
    id). Nothing is persisted or cached: edits to the analysis, the labels or
    their product links reflect on the next request.

    **What it derives.** Facings are the FRONT-ROW shelved objects of the
    analysis's stored board-first shelf composition (composed on the fly for
    legacy analyses that predate it), laid out along the scene's shelf axis û.
    From them:
    - `shelves[]` — facing counts, Σ linear cm, occupied span, utilization.
    - `share_of_shelf` — rollup rows per vocabulary kind: `by_label` always;
      `by_label_group` / `by_brand` / `by_category` / `by_subcategory` /
      `by_product` appear only when the scene's labels LINK to entities of
      that kind (links live on `ai-object-detection-label`).
    - `blocking` — "merchandised together?": maximal same-shelf runs of a
      group's facings, merged vertically across adjacent shelves when their
      u-intervals overlap; interrupting labels are ranked.
    - `objects` (`include_objects=true`) — planogram-style facing dump per
      shelf, positions re-based to each shelf's left edge (cm).
    - `adjacency` (`include_adjacency=true`) — who stands next to whom.

    **Questions (POST).** Up to 20 free-text or structured questions answered
    in order. Intent = share_of_shelf / blocking / adjacency; the target
    resolves by id → exact name → substring → fuzzy tokens against the
    scene's vocabulary; unresolved answers carry `reason` + `suggestions`.
    Each answer includes a human-readable `narrative`.

    **Config.** `default_facing_width_m` substitutes facings with no measured
    size; `block_min_overlap_m` / `block_overlap_frac` tune the vertical
    block merge. (`min_shelf_gap_m` / `shelf_gap_height_factor` are echoed
    for contract compatibility — shelf levels now come from the analysis's
    stored composition, tuned via the analysis `shelf_*` settings.)

    Positions are metres (AR world frame); product sizes centimetres.
    Namespace-scoped via the caller's token; read-only (PUT/PATCH/DELETE are
    rejected). Admins read any session; a REP token reads only insights of
    sessions THAT REP scanned (the mobile Retail Insights screen's read).
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /ai-object-detection-session-insight:
    get:
      summary: Compute the insight for a session / analysis
      operationId: findAiObjectDetectionSessionInsight
      parameters:
        - in: query
          name: session
          description: "Session `_id` — reads its LATEST successful analysis. One of `session` / `analysis` is required."
          schema: { type: string }
        - in: query
          name: analysis
          description: "Explicit analysis `_id` (must have `status: success`)."
          schema: { type: string }
        - in: query
          name: include_objects
          description: "Add the planogram-style facing dump per shelf (only the literal `true` is honoured)."
          schema: { type: boolean }
        - in: query
          name: include_adjacency
          description: "Add label-level neighbour counts (only the literal `true` is honoured)."
          schema: { type: boolean }
      responses:
        "200":
          description: The freshly computed insight payload (a single object — not paginated).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionInsight"
        "400":
          description: Neither session nor analysis given, or the analysis is not successful.
        "403":
          description: A REP token asked for a session that rep did not scan.
        "404":
          description: No matching analysis (or the session has no successful analysis yet).
    post:
      summary: Compute the insight and answer questions
      operationId: createAiObjectDetectionSessionInsight
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                session:
                  type: string
                  description: "Session `_id` — one of `session` / `analysis` is required."
                analysis: { type: string }
                questions:
                  type: array
                  maxItems: 20
                  description: Free-text strings and/or structured questions, answered in order.
                  items:
                    oneOf:
                      - type: string
                      - $ref: "#/components/schemas/InsightQuestion"
                config:
                  $ref: "#/components/schemas/InsightConfig"
                include_objects: { type: boolean }
                include_adjacency: { type: boolean }
      responses:
        "201":
          description: "The insight payload plus `answers[]` (one per question, in order)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionInsight"
        "400":
          description: "Neither session nor analysis given, more than 20 questions, or the analysis is not successful."
        "403":
          description: A REP token asked for a session that rep did not scan.
        "404":
          description: No matching analysis (or the session has no successful analysis yet).
  /ai-object-detection-session-insight/{id}:
    get:
      summary: Compute the insight for a session (deep link)
      description: The path id is read as a SESSION id.
      operationId: getAiObjectDetectionSessionInsight
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The freshly computed insight payload.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionInsight"
        "403":
          description: A REP token asked for a session that rep did not scan.
        "404":
          description: The session has no successful analysis yet.
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: api-key
      description: |
        Server-issued API key. Also accepted via the `x-api-key` header or the
        `?apiKey=` query parameter as fallbacks.
    JwtAuth:
      type: apiKey
      in: header
      name: Authorization
      description: |
        Raw JWT in the `Authorization` header — **no `Bearer ` prefix**.
        Obtained from `POST /authenticate` (admin / rep / client login).
  schemas:
    InsightConfig:
      type: object
      description: All optional — invalid values fall back to the defaults.
      properties:
        min_shelf_gap_m:
          type: number
          default: 0.06
          description: Echoed for compatibility — shelf levels come from the stored composition.
        shelf_gap_height_factor:
          type: number
          default: 0.35
          description: Echoed for compatibility.
        default_facing_width_m:
          type: number
          default: 0.08
          description: Fallback facing width (m) when a facing has no measured size.
        block_min_overlap_m:
          type: number
          default: 0.03
          description: Vertical block merge — required axis overlap (m) between adjacent-shelf runs…
        block_overlap_frac:
          type: number
          default: 0.5
          description: "…or this fraction of the narrower run's width, whichever is smaller."
    InsightQuestion:
      type: object
      required: [type, target]
      properties:
        type:
          type: string
          enum: [share_of_shelf, blocking, adjacency]
        target:
          type: object
          description: Resolved by `id` first, then exact name, then fuzzy match.
          properties:
            kind:
              type: string
              enum: [label, label_group, brand, category, subcategory, product]
            id: { type: string }
            name: { type: string }
    InsightShareRow:
      type: object
      properties:
        key: { type: string }
        kind:
          type: string
          enum: [label, label_group, brand, category, subcategory, product]
        name: { type: string }
        facings: { type: number }
        linear_cm: { type: number }
        area_cm2: { type: number }
        facing_share:
          { type: number, description: 0..1 of all detected facings. }
        linear_share: { type: number }
        area_share: { type: number }
        shelves:
          type: array
          items:
            type: object
            properties:
              shelf_index: { type: number }
              facings: { type: number }
              linear_cm: { type: number }
              linear_share_of_shelf: { type: number }
        labels:
          type: array
          items:
            type: object
            properties:
              label_id: { type: string }
              label_name: { type: string }
              facings: { type: number }
    InsightBlocking:
      type: object
      properties:
        key: { type: string }
        name: { type: string }
        narrative: { type: string }
        present: { type: boolean }
        facings: { type: number }
        labels_present:
          type: array
          items:
            type: object
            properties:
              label_id: { type: string }
              label_name: { type: string }
              facings: { type: number }
        blocks_count: { type: number }
        is_single_block:
          type: boolean
          description: True when every facing of the group sits in ONE contiguous block.
        largest_block_facings: { type: number }
        largest_block_share: { type: number }
        blocks:
          type: array
          items:
            type: object
            properties:
              shelves:
                type: array
                items: { type: number }
              facings: { type: number }
              runs:
                type: array
                items:
                  type: object
                  properties:
                    shelf_index: { type: number }
                    t0: { type: number, description: Left edge along û (m). }
                    t1: { type: number }
                    facings: { type: number }
                    facing_ids:
                      type: array
                      items: { type: string }
        interrupters:
          type: array
          description: Foreign labels breaking the block, ranked by interrupting facings.
          items:
            type: object
            properties:
              label_id: { type: string }
              label_name: { type: string }
              facings: { type: number }
        shelves_spanned:
          type: array
          items: { type: number }
    SessionInsight:
      type: object
      properties:
        session: { type: string }
        analysis: { type: string }
        model_version: { type: string }
        analysis_finished_at: { type: number }
        computed_at:
          type: number
          description: Unix ms — always freshly computed, never cached.
        config_used:
          $ref: "#/components/schemas/InsightConfig"
        scene:
          type: object
          properties:
            facings_placed: { type: number }
            objects_skipped:
              type: number
              description: Analysis objects not counted as facings (back rows / unshelved).
            shelf_count: { type: number }
            axis:
              type: object
              description: Unit shelf-axis direction û in the horizontal plane.
              properties:
                dir_x: { type: number }
                dir_z: { type: number }
        totals:
          type: object
          properties:
            facings: { type: number }
            linear_cm: { type: number }
            area_cm2: { type: number }
        shelves:
          type: array
          items:
            type: object
            properties:
              index: { type: number, description: 0 = bottom shelf. }
              y_base_m:
                { type: number, description: Board level (m, AR world frame). }
              facings: { type: number }
              linear_cm: { type: number }
              span_cm: { type: number }
              utilization:
                type: number
                description: linear_cm / span_cm — how much of the used span is product.
        share_of_shelf:
          type: object
          description: "Keys: `by_label` always; `by_label_group` / `by_brand` / `by_category` / `by_subcategory` / `by_product` only when links exist."
          additionalProperties:
            type: array
            items:
              $ref: "#/components/schemas/InsightShareRow"
        blocking:
          type: object
          description: Same keys as share_of_shelf; rows only for present groups.
          additionalProperties:
            type: array
            items:
              $ref: "#/components/schemas/InsightBlocking"
        objects:
          type: array
          description: "Only with `include_objects=true` — facings left→right per shelf."
          items:
            type: object
            properties:
              shelf_index: { type: number }
              facings:
                type: array
                items:
                  type: object
                  properties:
                    id: { type: string }
                    label_id: { type: string }
                    label_name: { type: string }
                    position: { type: number }
                    from_cm:
                      type: number
                      description: Re-based to the shelf's left-most facing.
                    to_cm: { type: number }
                    w_cm: { type: number }
                    h_cm: { type: number }
                    confidence: { type: number }
        adjacency:
          type: array
          description: "Only with `include_adjacency=true`."
          items:
            type: object
            properties:
              label_id: { type: string }
              label_name: { type: string }
              neighbors:
                type: array
                items:
                  type: object
                  properties:
                    label_id: { type: string }
                    label_name: { type: string }
                    count: { type: number }
        answers:
          type: array
          description: Present on POST responses only — one per question, in order.
          items:
            type: object
            properties:
              question:
                oneOf:
                  - type: string
                  - $ref: "#/components/schemas/InsightQuestion"
              type:
                type: string
                enum: [share_of_shelf, blocking, adjacency]
              resolved: { type: boolean }
              reason: { type: string }
              suggestions:
                type: array
                items: { type: string }
              target:
                type: object
                properties:
                  kind: { type: string }
                  name: { type: string }
                  label_ids:
                    type: array
                    items: { type: string }
              result:
                description: Share row, blocking result, or neighbour list per the question type.
                oneOf:
                  - $ref: "#/components/schemas/InsightShareRow"
                  - $ref: "#/components/schemas/InsightBlocking"
                  - type: object
              narrative: { type: string }
