openapi: 3.0.3
info:
  title: Repzo API - AI Object Detection Session Election
  version: 1.0.0
  description: |
    Materializes an election-playground result as a NEW object-detection
    session. The dashboard's election playground simulates stack-then-elect
    frame election over a session captured in the AR app's election-debug mode
    (every stacked frame uploaded); this endpoint saves the elected subset as
    a real session so it can go through the normal analysis pipeline.

    **What it does.** The source session document is cloned — `device`,
    `capture_settings`, `coverage_m2`, `client` and `status` copied,
    `session_id` suffixed `-E<n>` (n = 1 + prior elections of that source)
    and `source_session` set for provenance. The elected tasks are first
    SCREENED against the namespace detection-settings ERROR tier in capture
    order (`sharpness`, `tracking`, `too_near`/`too_far`, `yaw_delta`/
    `pitch_delta`/`roll_delta` vs the previous kept frame); excluded frames
    stay on the source session (they keep counting toward coverage) and are
    listed under `election_excluded`. The surviving tasks are duplicated onto
    the new session referencing the SAME media blobs (no image/depth copies)
    with annotation state reset, so inference re-runs from scratch.

    **Session gates & verdict.** `rejection_reasons` collects the session
    gates that fired — `coverage_below_target` (device `coverage_m2` below the
    configured target), `jump_detected` (adjacent kept frames farther apart
    than a frame footprint, unless `allow_jump`), `too_many_elected` (kept
    frames exceed coverage / avg footprint × allowance). `session_score` is
    the average frame quality over ALL frames of the source session (0..1)
    and `session_verdict` is `rejected` when any gate fired, else banded from
    the score (`excellent` / `good` / `acceptable`). A `rejected` verdict
    also suppresses the category auto-analysis on upload-complete.

    **Who calls it & lifecycle.** Admin / API-key callers from the dashboard.
    Create-only — `find`/`get`/`update`/`patch`/`remove` return 405. The new
    row lives in the regular `ai-object-detection-session` collection
    (namespace-scoped via the caller's token; `company_namespace` is
    server-injected) and is read/managed through that service.
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /ai-object-detection-session-election:
    post:
      summary: Save an elected frame subset as a new session
      description: >-
        Clones the source session and the chosen tasks (after error-tier
        screening) into a new session with the same media references and a
        fresh annotation state. Returns the new session document.
      operationId: createAiObjectDetectionSessionElection
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ElectionRequest"
      responses:
        "201":
          description: The newly created session document (a regular `ai-object-detection-session` row).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ElectedSession"
        "400":
          description: >-
            Missing `session`, empty `task_ids`, more than 500 task ids, task
            ids that do not belong to the source session, or every elected
            frame fails the error-tier limits (nothing to materialize).
        "404":
          description: Source session not found in the caller's namespace.
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:
    ElectionRequest:
      type: object
      required: [session, task_ids]
      properties:
        session:
          type: string
          description: Source `ai-object-detection-session` `_id`.
        task_ids:
          type: array
          minItems: 1
          maxItems: 500
          items: { type: string }
          description: >-
            Elected task `_id`s — must all belong to the source session.
        config_snapshot:
          type: object
          additionalProperties: true
          description: >-
            Optional playground formula parameters used for this election.
            Accepted for provenance only — not persisted on the session.
        company_namespace:
          type: array
          items: { type: string }
          description: Optional tenant namespace override for SDK callers.
    UserRef:
      type: object
      description: Compact actor reference (rep or admin).
      properties:
        _id: { type: string }
        type: { type: string, enum: [admin, rep] }
        name: { type: string }
        rep: { type: string }
        admin: { type: string }
    ElectedSession:
      type: object
      description: >-
        The materialized session — a regular `ai-object-detection-session`
        document (see that service's `SessionSchema` for every field); the
        election-specific fields are always present here.
      properties:
        _id: { type: string }
        session_id:
          type: string
          description: "Source id with an election suffix, e.g. `AB12CD34-E1`."
        source_session:
          type: string
          description: The source session `_id` (provenance).
        status:
          type: string
          enum:
            [open, uploaded, infer_in_progress, inferred, rearbitrating, failed]
          description: Copied from the source session.
        device: { type: object, additionalProperties: true }
        capture_settings: { type: object, additionalProperties: true }
        coverage_m2:
          type: number
          description: Copied from the source (total coverage INCLUDES the excluded frames).
        client: { type: string }
        session_score:
          type: number
          description: Average frame quality over ALL frames of the source session, 0..1.
        session_verdict:
          type: string
          enum: [excellent, good, acceptable, rejected]
        rejection_reasons:
          type: array
          items:
            type: string
            enum: [coverage_below_target, jump_detected, too_many_elected]
        election_excluded:
          type: array
          description: Error-tier frames excluded from the clone.
          items:
            type: object
            properties:
              task: { type: string }
              violations:
                type: array
                items:
                  type: string
                  enum:
                    [
                      sharpness,
                      tracking,
                      too_near,
                      too_far,
                      yaw_delta,
                      pitch_delta,
                      roll_delta,
                    ]
        frames_total: { type: number }
        frames_accepted: { type: number }
        tasks_count: { type: number }
        detections_count: { type: number }
        objects_count: { type: number }
        creator:
          $ref: "#/components/schemas/UserRef"
        disabled: { type: boolean }
        company_namespace:
          type: array
          items: { type: string }
          description: Tenant key. Server-injected — never accept from clients.
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
