openapi: 3.0.3
info:
  title: Repzo API - AI Object Detection Detection Settings
  version: 1.0.0
  description: |
    **Unified detection settings** — the per-namespace configuration of the
    AR shelf-scan CAPTURE pipeline: how the mobile app captures frames
    (`capture`: mode, rate, resolution, torch, exposure lock), the two-tier
    per-frame quality limits (`frame`: sharpness, yaw/pitch/roll deltas,
    depth variation, distance, tracking score — each with a `warn` bound that
    only shows guidance and an `error` bound that excludes the frame from
    election while it still counts toward coverage), the frame-election
    formula (`election`: coverage target, sliver fraction, redundancy exponent
    and quality weights) and the session acceptance gates (`session`:
    coverage target, jump tolerance, elected-frame budget, verdict score
    bands). It is the single source of truth for knobs that used to live
    scattered across the mobile validator, the election engine and the
    backend session gates.

    **Who calls it.** The MOBILE app pulls `GET` on session start with a rep
    token (reads are open to any authenticated scope); the dashboard (admin)
    writes. The auto-seeded permission grid governs per-scope access.

    **Multi-tenancy & lifecycle.** One logical document per
    `company_namespace` (keyed from the caller's token — never sent in the
    body; unique index). The stored document holds only PARTIAL OVERRIDES
    over the engine defaults (`DEFAULT_DETECTION_CONFIG`); every read returns
    the MERGED result (`config`) alongside the raw `overrides`, so partial
    overrides stay partial and new engine defaults reach every namespace
    automatically. `GET` never 404s — an untouched namespace answers
    `is_default: true` with the pure defaults. `POST` upserts (`PUT` / `PATCH`
    behave identically and ignore the path id) and REPLACES the stored
    overrides object wholesale (it is not merged into previously saved
    overrides — send the complete set you want kept). `DELETE` resets to the
    factory defaults by dropping the document (id ignored) and answers only
    `{ is_default: true, config }`. No query parameters are honoured;
    responses are single objects, never paginated.

    **Relationships.** Frames submitted by the app stamp
    `frame_meta.detection_settings_polled_at` on their
    `ai-object-detection-task` so an analysis can tell which settings a
    capture ran under; the backend election
    (`ai-object-detection-session-election`) enforces the same `frame` /
    `election` / `session` values server-side.

    **Not to be confused with** `ai-object-detection-settings`, which stores
    the dashboard-only ANALYZE defaults (scene-math knobs) and never reaches
    the device.
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /ai-object-detection-detection-settings:
    get:
      summary: Read the namespace's merged detection config
      description: |
        Returns the engine defaults deep-merged with the namespace's saved
        overrides, plus the raw overrides. Never 404s. Query parameters are
        ignored. This is what the mobile app pulls on session start.
      operationId: findAiObjectDetectionDetectionSettings
      responses:
        "200":
          description: The merged detection config (or the pure defaults when nothing is saved).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetectionSettings"
    post:
      summary: Save the namespace's detection overrides
      description: |
        Upserts the single document for the caller's namespace, REPLACING the
        stored overrides object with `config`, and returns the fresh merged
        read. `config` must be an object (else `400`); any subset of the
        `DetectionConfig` tree is accepted and deep-merged over the defaults
        on read.
      operationId: createAiObjectDetectionDetectionSettings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DetectionSettingsCreateBody"
      responses:
        "201":
          description: The merged config, re-read after the upsert.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetectionSettings"
        "400":
          description: "`config` is missing or not an object."
  /ai-object-detection-detection-settings/{id}:
    get:
      summary: Read the merged detection config (id ignored)
      description: One logical document per namespace — this mirrors the collection read.
      operationId: getAiObjectDetectionDetectionSettings
      parameters:
        - in: path
          name: id
          required: true
          description: Ignored — there is a single document per namespace.
          schema: { type: string }
      responses:
        "200":
          description: Same shape as the collection read.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetectionSettings"
    put:
      summary: Save the detection overrides (id ignored)
      description: Identical to POST — upserts the namespace's single document.
      operationId: updateAiObjectDetectionDetectionSettings
      parameters:
        - in: path
          name: id
          required: true
          description: Ignored — the namespace keys the document.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DetectionSettingsWrite"
      responses:
        "200":
          description: The merged config, re-read after the upsert.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetectionSettings"
        "400":
          description: "`config` is missing or not an object."
    patch:
      summary: Save the detection overrides (identical to PUT)
      operationId: patchAiObjectDetectionDetectionSettings
      parameters:
        - in: path
          name: id
          required: true
          description: Ignored — the namespace keys the document.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DetectionSettingsWrite"
      responses:
        "200":
          description: The merged config, re-read after the upsert.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetectionSettings"
        "400":
          description: "`config` is missing or not an object."
    delete:
      summary: Reset to the factory defaults
      description: |
        Drops the namespace's overrides document. The id is ignored. Note the
        reduced response shape — only `is_default` and the default `config`.
      operationId: removeAiObjectDetectionDetectionSettings
      parameters:
        - in: path
          name: id
          required: true
          description: Ignored — any value works.
          schema: { type: string }
          example: defaults
      responses:
        "200":
          description: Reset acknowledgement with the pure engine defaults.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetectionSettingsResetResult"
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:
    WarnError:
      type: object
      description: |
        Two-tier frame limit. `warn` = guidance shown on the device, frame
        still usable; `error` = frame excluded from election (it still counts
        toward the coverage total). `warn` is always the softer bound.
      required: [warn, error]
      properties:
        warn: { type: number }
        error: { type: number }
    CaptureConfig:
      type: object
      properties:
        mode:
          type: string
          enum: [stack_elect, continuous, burst]
          default: stack_elect
        rate_hz:
          type: number
          default: 4
          description: Capture rate, frames per second.
        resolution:
          type: string
          enum: [medium, high, max]
          default: max
        stack_timeout_s:
          type: number
          default: 30
          description: Seconds before a stack-elect capture times out.
        sweep_guide:
          type: boolean
          default: true
          description: Show the sweep guide overlay.
        torch:
          type: boolean
          default: true
        exposure_lock:
          type: boolean
          default: true
    FrameConfig:
      type: object
      description: Per-frame quality limits, each a `WarnError` pair.
      properties:
        min_sharpness:
          allOf:
            - $ref: "#/components/schemas/WarnError"
          description: "Variance-of-Laplacian sharpness floor. `warn` = live-guidance floor, `error` = election hard floor (the blur limit). Default `{ warn: 200, error: 50 }`."
        max_yaw_delta_deg:
          allOf:
            - $ref: "#/components/schemas/WarnError"
          description: "Degrees. Default `{ warn: 18, error: 30 }`."
        max_pitch_delta_deg:
          allOf:
            - $ref: "#/components/schemas/WarnError"
          description: "Degrees. Default `{ warn: 15, error: 28 }`."
        max_roll_delta_deg:
          allOf:
            - $ref: "#/components/schemas/WarnError"
          description: "Degrees. Default `{ warn: 12, error: 25 }`."
        max_depth_variation_m:
          allOf:
            - $ref: "#/components/schemas/WarnError"
          description: "Metres. Default `{ warn: 0.8, error: 1.5 }`."
        min_distance_m:
          allOf:
            - $ref: "#/components/schemas/WarnError"
          description: "Metres — `error` is the NEARER bound (worse). Default `{ warn: 0.5, error: 0.3 }`."
        max_distance_m:
          allOf:
            - $ref: "#/components/schemas/WarnError"
          description: "Metres — `error` is the FARTHER bound (worse). Default `{ warn: 2.0, error: 3.0 }`."
        min_tracking_score:
          allOf:
            - $ref: "#/components/schemas/WarnError"
          description: "0..100. Default `{ warn: 60, error: 35 }`."
    ElectionConfig:
      type: object
      properties:
        cover_target:
          type: number
          default: 0.985
          description: Stop electing at this union coverage (0..1).
        sliver_frac:
          type: number
          default: 0.02
          description: Ignore gains under this fraction of a median footprint.
        gain_exp:
          type: number
          default: 1.5
          description: "Redundancy penalty exponent (score = quality · gain^exp)."
        w_sharp:
          type: number
          default: 0.4
          description: Quality geometric-mean weight — sharpness.
        w_depth:
          type: number
          default: 0.3
          description: Quality geometric-mean weight — depth.
        w_track:
          type: number
          default: 0.2
          description: Quality geometric-mean weight — tracking.
        w_lux:
          type: number
          default: 0.1
          description: Quality geometric-mean weight — lux.
    SessionScoreBands:
      type: object
      description: "Average-quality verdict bands; below `acceptable` = rejected."
      properties:
        excellent: { type: number, default: 0.8 }
        good: { type: number, default: 0.65 }
        acceptable: { type: number, default: 0.45 }
    SessionConfig:
      type: object
      properties:
        coverage_target_m2:
          type: number
          default: 0.5
          description: Reject when the device-reported covered area (m²) is below this.
        allow_jump:
          type: boolean
          default: false
          description: When false, a spatial jump (after removing error frames) rejects the session.
        elected_allowance:
          type: number
          default: 1.6
          description: "Elected-count budget: ceil(total_area / avg_frame_area × allowance). More elected frames than that rejects the session."
        score:
          $ref: "#/components/schemas/SessionScoreBands"
    DetectionConfig:
      type: object
      description: The full merged detection config (defaults deep-merged with the namespace overrides).
      properties:
        capture:
          $ref: "#/components/schemas/CaptureConfig"
        frame:
          $ref: "#/components/schemas/FrameConfig"
        election:
          $ref: "#/components/schemas/ElectionConfig"
        session:
          $ref: "#/components/schemas/SessionConfig"
    DetectionConfigOverrides:
      type: object
      description: |
        Deep-partial subset of `DetectionConfig`. Objects merge recursively
        over the defaults, scalars replace (a `WarnError` may carry only
        `warn` or only `error`). Unknown keys are kept by the merge so the
        config can grow.
      properties:
        capture:
          $ref: "#/components/schemas/CaptureConfig"
        frame:
          $ref: "#/components/schemas/FrameConfig"
        election:
          $ref: "#/components/schemas/ElectionConfig"
        session:
          $ref: "#/components/schemas/SessionConfig"
      example:
        capture: { torch: false }
        frame: { min_sharpness: { error: 80 } }
        session: { coverage_target_m2: 0.8 }
    DetectionSettings:
      type: object
      description: Response of find / get / create / update / patch.
      properties:
        _id:
          type: string
          nullable: true
          description: "`null` while the namespace has never saved overrides."
        is_default:
          type: boolean
          description: "true = no overrides saved; `config` is the pure engine default."
        config:
          $ref: "#/components/schemas/DetectionConfig"
        overrides:
          allOf:
            - $ref: "#/components/schemas/DetectionConfigOverrides"
          description: "The raw saved overrides (`{}` when `is_default`)."
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: "When the overrides were last saved; `null` when `is_default`."
    DetectionSettingsWrite:
      type: object
      description: PUT / PATCH body — the same single document is upserted.
      required: [config]
      properties:
        config:
          $ref: "#/components/schemas/DetectionConfigOverrides"
    DetectionSettingsCreateBody:
      description: |
        POST body. The tenant key is taken from the caller's token; the
        optional `company_namespace` exists for SDK-call uniformity and is not
        read by this endpoint.
      allOf:
        - $ref: "#/components/schemas/DetectionSettingsWrite"
        - type: object
          properties:
            company_namespace:
              type: array
              items: { type: string }
              description: Optional tenant namespace override for SDK callers. Not read by this endpoint — the document is keyed from the caller's token namespace.
    DetectionSettingsResetResult:
      type: object
      description: "DELETE response — note there is no `_id` / `overrides` / `updatedAt`."
      properties:
        is_default:
          type: boolean
          enum: [true]
        config:
          $ref: "#/components/schemas/DetectionConfig"
