openapi: 3.0.3
info:
  title: Repzo API - AI Object Detection Task
  version: 1.0.0
  description: |
    **Object Detection Tasks** are the atomic annotatable unit of the
    AI object-detection pipeline. Each task is one image plus its
    `annotation_groups[]` — versioned sets of 2D bounding-box
    `annotations[]` (each with a `box`, `confidence`, `label_id`, and
    `label_state`). Tasks feed datasets, model-version training, and
    the dashboard review canvas.

    **Sessions (AR shelf scans).** A task may optionally belong to an
    `ai-object-detection-session` (set via the `session` field). When it
    does, it also carries per-frame AR context in `frame_meta` (pose,
    intrinsics, tracking, depth source), raw `depth_media` /
    `confidence_media` blobs, and `depth_shape`. When such a frame is
    inferred (via `ai-object-detection-inference` with a `model_version`,
    directly or driven by `ai-object-detection-session-analysis`), each
    placeable detection is back-projected into world coordinates: individual
    `annotations[]` gain `world_position`, `world_size`, `depth_at_center`,
    `depth_confidence`, and `placement_confidence` (geometry only — the
    kept/ignored/merged conclusion is made by the analysis stage, not stored
    here). Human-made / edited boxes (`manual` / `auto_edited` groups) get the
    same placement at SAVE time: a box whose geometry no longer matches its
    `placed_box` snapshot is re-placed on the next `PUT`. After a successful
    analysis run, each contributing annotation's `cluster_id` points at the
    concluded object (`objects[]._id`) on the
    `ai-object-detection-session-analysis` document. All of these fields
    are additive and absent on tasks created from the dashboard upload flow.

    **Who calls it.** Admins and reps from the dashboard (upload, review,
    bulk-approve). Session frames are materialized as tasks by the
    `activity-ai-object-detection-session-frame` intake service.

    **Multi-tenancy & lifecycle.** Records are scoped by
    `company_namespace[]` (server-injected on create / update). Soft-delete via
    `disabled: true` (DELETE flips the flag). `annotated` is derived on write:
    `true` when any confirmed group has annotations. `creator` is stamped on
    create, `editor` on update. A `PUT` that really changes a session frame's
    human annotations (moved / new boxes, adds / deletes, re-labels, a fresh
    group) schedules a re-analysis of the parent session; flag flips do not.

    **Key relationships.** References `media-storage`
    (`file_media`, `depth_media`, `confidence_media`, XAI images),
    `ai-object-detection-label` (`annotations.label_id`, `original_label`),
    `ai-object-detection-model-version` (`annotation_groups.model_version`,
    `annotated_by_model_version_code`), `ai-object-detection-dataset`
    (`task_dataset.dataset`), and `ai-object-detection-session` (`session`).

    **Bulk patch.** `PATCH /ai-object-detection-task` (no id) accepts a
    `patch-action`-style `writeQuery` and applies it to all tasks matching
    the query filters (the same keys as find, e.g. `?session=:id` to approve
    all auto-annotations for a session). Returns `{ nFound, nModified }`.

    **Population.** `?populatedKeys[]=` supports `file_media`,
    `task_dataset.dataset`, `annotation_groups.model_version`,
    `annotation_groups.annotations.label_id`,
    `annotation_groups.annotations.original_label`,
    `annotation_groups.annotated_by_model_version_code`, `session`,
    `depth_media`, `confidence_media`,
    `annotation_groups.xai.heatmap_media`,
    `annotation_groups.xai.gradcam_media`,
    `annotation_groups.xai.gradcam_per_detection.media`, and
    `annotation_groups.xai.feature_maps.media`. TOP-LEVEL refs
    (`file_media`, `session`, `depth_media`, `confidence_media`) are emitted
    under `<field>_populated` while the ref itself stays an id; every NESTED
    path (under `task_dataset.` / `annotation_groups.`) populates INLINE,
    replacing the id with the document.
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /ai-object-detection-task:
    get:
      summary: Find object detection tasks
      operationId: findAiObjectDetectionTasks
      parameters:
        - in: query
          name: _id
          description: |
            Filter by task `_id`. Pass once for a single match, or as
            `?_id[]=...&_id[]=...` for multiple.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: session
          description: Filter tasks (frames) belonging to a session `_id`.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: frame_meta.frame_id
          description: Filter by the session-scoped frame counter.
          schema:
            oneOf:
              - type: number
              - type: array
                items: { type: number }
        - in: query
          name: task_dataset.dataset
          description: Filter tasks belonging to a dataset `_id`.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: annotated
          description: Filter by whether the task has any confirmed annotations.
          schema: { type: boolean }
        - in: query
          name: annotated_label
          description: |
            Only tasks containing at least one annotation (in any group) whose
            `label_id` is one of the given label `_id`s. Used to surface
            candidate crops for a label's reference photo. Non-ObjectId values
            are ignored.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: annotation_status
          description: |
            Filter by the task's mutually-exclusive annotation workflow status.
            The filter is applied before counting and pagination. `pending`
            means the task is not annotated (or has no annotation groups);
            `confirmed` means its leading annotation group is confirmed;
            `manual` and `auto_edited` select annotated tasks whose leading
            group is unconfirmed with that state; `auto` selects the remaining
            annotated tasks with an unconfirmed leading group. Any other value
            is rejected with `400`.
          schema:
            type: string
            enum: [pending, confirmed, manual, auto_edited, auto]
        - in: query
          name: disabled
          description: "Include soft-deleted tasks. Omitted / `false` returns active tasks only; `true` returns disabled tasks only."
          schema: { type: boolean, default: false }
        - in: query
          name: from_createdAt
          description: Return tasks created at/after this Unix timestamp (ms) or date string.
          schema: { type: number }
        - in: query
          name: to_createdAt
          description: Return tasks created at/before this Unix timestamp (ms) or date string.
          schema: { type: number }
        - in: query
          name: from_updatedAt
          description: Cursor — tasks with `updatedAt` at/after this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: to_updatedAt
          description: Cursor — tasks with `updatedAt` at/before this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: populatedKeys
          description: |
            Embed referenced documents. Top-level refs land under
            `<field>_populated`; nested paths populate inline (see the intro).
          schema:
            type: array
            items:
              type: string
              enum:
                - file_media
                - task_dataset.dataset
                - annotation_groups.model_version
                - annotation_groups.annotations.label_id
                - annotation_groups.annotations.original_label
                - annotation_groups.annotated_by_model_version_code
                - session
                - depth_media
                - confidence_media
                - annotation_groups.xai.heatmap_media
                - annotation_groups.xai.gradcam_media
                - annotation_groups.xai.gradcam_per_detection.media
                - annotation_groups.xai.feature_maps.media
        - in: query
          name: per_page
          description: Page size. Defaults to the server's configured pagination limit.
          schema: { type: integer, minimum: 1, maximum: 50000 }
          example: 50
        - in: query
          name: page
          description: 1-indexed page number.
          schema: { type: integer, minimum: 1 }
          example: 1
        - in: query
          name: sort
          description: "Field to sort by. Defaults to `_id`."
          schema: { type: string, default: _id }
        - in: query
          name: sortPageOrder
          description: "Sort direction. Defaults to descending (`dsc`)."
          schema: { type: string, enum: [asc, dsc], default: dsc }
      responses:
        "200":
          description: Paginated list of object detection tasks.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskFindResult"
        "400":
          description: "Invalid `annotation_status` value."
    post:
      summary: Create an object detection task
      description: |
        Creates a task. `company_namespace` and `creator` are
        server-injected. Each `annotation_group` MUST carry a valid
        `annotation_state` (else `400`): `manual` / `auto_edited` mark the
        group `confirmed` (+ `confirmed_by` from the JWT), `auto` records
        `annotated_by_model_version_code` = `model_version`. A missing
        `time` defaults to now. `annotated` is derived.
      operationId: createAiObjectDetectionTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskCreateBody"
      responses:
        "201":
          description: The newly-created task document.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSchema"
        "400":
          description: "An annotation group has a missing / invalid `annotation_state`, or model validation failed (e.g. missing `file_media`)."
    patch:
      summary: Bulk-update tasks
      description: |
        Bulk update via the `patch-action` `writeQuery` shape, applied to
        all tasks matching the query filters (the same accepted keys as
        find — no pagination / population / `annotation_status`). Used for
        session-scoped bulk actions (e.g. `?session=:id` with a writeQuery
        that sets annotation groups confirmed). Returns `{ nFound, nModified }`.
      operationId: patchAiObjectDetectionTask
      parameters:
        - in: query
          name: _id
          description: Restrict the bulk update to these task `_id`s.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: session
          description: Restrict the bulk update to tasks of this session `_id`.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: frame_meta.frame_id
          description: Restrict to the given session-scoped frame counter(s).
          schema:
            oneOf:
              - type: number
              - type: array
                items: { type: number }
        - in: query
          name: task_dataset.dataset
          description: Restrict to tasks belonging to a dataset `_id`.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: annotated
          schema: { type: boolean }
        - in: query
          name: disabled
          schema: { type: boolean, default: false }
        - in: query
          name: from_createdAt
          schema: { type: number }
        - in: query
          name: to_createdAt
          schema: { type: number }
        - in: query
          name: from_updatedAt
          schema: { type: number }
        - in: query
          name: to_updatedAt
          schema: { type: number }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchActionBody"
      responses:
        "200":
          description: Bulk-update result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PatchActionResult"
        "400":
          description: "Unknown `writeQuery[].command`."
  /ai-object-detection-task/{id}:
    get:
      summary: Get a task by id
      operationId: getAiObjectDetectionTask
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: query
          name: populatedKeys
          description: Same keys as on find.
          schema:
            type: array
            items:
              type: string
              enum:
                - file_media
                - task_dataset.dataset
                - annotation_groups.model_version
                - annotation_groups.annotations.label_id
                - annotation_groups.annotations.original_label
                - annotation_groups.annotated_by_model_version_code
                - session
                - depth_media
                - confidence_media
                - annotation_groups.xai.heatmap_media
                - annotation_groups.xai.gradcam_media
                - annotation_groups.xai.gradcam_per_detection.media
                - annotation_groups.xai.feature_maps.media
      responses:
        "200":
          description: The task document for the given `_id`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSchema"
        "400":
          description: No task with that `_id` in the caller's namespace (note — 400, not 404).
    put:
      summary: Update a task
      description: |
        Standard put — typically to edit annotations during review. The body's
        `annotation_groups` REPLACES the stored array wholesale, so clients
        must resend every group they want to keep; revising an existing group
        means resending it edited in place (not appending a copy).
        `editor` is server-injected; `annotated` is re-derived; `edit_time`
        is restamped on every group; an `auto_edited` group is marked
        `confirmed`; each confirmed group's `confirmed_by` is stamped from
        the JWT only when it doesn't already carry a valid user — the
        original confirmer is preserved across re-saves. On session frames,
        human boxes (`manual` / `auto_edited` groups) whose geometry changed
        since their `placed_box` are re-placed in world coordinates
        (best-effort — a failure records `ignore_reason` and never blocks
        the save), and a real content change schedules a session
        re-analysis.
      operationId: updateAiObjectDetectionTask
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskUpdateBody"
      responses:
        "200":
          description: The task document after the update is applied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSchema"
        "404":
          description: No task with that `_id` in the caller's namespace.
    delete:
      summary: Soft-delete a task
      description: "Sets `disabled: true` and returns the task."
      operationId: removeAiObjectDetectionTask
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: "The task document after soft-deletion (`disabled: true`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSchema"
        "400":
          description: No task with that `_id` 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:
    UserRef:
      type: object
      properties:
        _id: { type: string }
        type: { type: string, enum: [admin, rep] }
        name: { type: string }
        rep: { type: string }
        admin: { type: string }
    Box:
      type: object
      description: "Normalized box. Inference and the dashboard store YOLO center-size here: `x1` = cx, `y1` = cy, `x2` = w, `y2` = h."
      required: [x1, x2, y1, y2]
      properties:
        x1: { type: number }
        x2: { type: number }
        y1: { type: number }
        y2: { type: number }
    Annotation:
      type: object
      required: [box, label_id, label_state]
      properties:
        _id: { type: string }
        box:
          $ref: "#/components/schemas/Box"
        confidence: { type: number, default: 0.25 }
        label_id:
          type: string
          description: "`ai-object-detection-label` id."
        label_state: { type: string, enum: [auto, manual] }
        placed_box:
          allOf:
            - $ref: "#/components/schemas/Box"
          description: |
            The box geometry world placement was last computed FOR. Set by the
            placement pass (inference for auto groups; the task-save pass for
            manual/auto_edited groups). When `box` differs from `placed_box`
            (a human moved/redrew it), the next save recomputes the world
            fields — so every annotation carries AR data from birth.
        world_position:
          type: object
          description: |
            Back-projected centroid in world coordinates, **metres**. Written
            by the PLACEMENT pass: at inference time for model detections, and
            at task-save time for human-made/edited boxes (manual /
            auto_edited groups on session frames with usable
            pose/intrinsics/depth).
          properties:
            x: { type: number }
            y: { type: number }
            z: { type: number }
        world_size:
          type: object
          description: "Physical front-face size of the detection, **centimetres** (`w` = world-horizontal, `h` = vertical)."
          properties:
            w: { type: number }
            h: { type: number }
        depth_at_center:
          type: number
          description: |
            Distance from the camera, **metres** (session frames only) — the
            front-biased percentile (default p30) of the depth pixels sampled
            over the whole detection bbox.
        depth_confidence:
          type: number
          description: Normalized 0..1 depth confidence (session frames only).
        placement_confidence:
          type: number
          description: "detection_conf x depth_conf x tracking (placed session frames only)."
        cluster_id:
          type: string
          description: |
            Link to the analysis object this detection was clustered into. Note:
            the kept/ignored/merged conclusion lives on the
            `ai-object-detection-session-analysis` doc, **not** on the annotation.
        ignore_reason:
          type: string
          enum:
            - no_pose
            - no_intrinsics
            - no_depth
            - empty_depth_region
            - insufficient_depth_pixels
            - behind_shelf
          description: |
            Why this detection could NOT be placed in world coordinates (set by
            the placement pass; unset when placed). Frame-level causes:
            `no_pose` / `no_intrinsics` / `no_depth`. Per-box causes:
            `empty_depth_region` (box maps to no depth pixels),
            `insufficient_depth_pixels` (fewer than 3 valid pixels),
            `behind_shelf` (only background depth beyond the shelf gate).
        original_label:
          type: string
          description: |
            Dims-reclassifier provenance — when the reclassifier moves this
            annotation to a sibling label, the FIRST original label is kept
            here (written once; cleared if a later run reverts to it).
        reclassification_reason:
          type: string
          enum: [dims_match_sibling, group_consensus, manual]
        size_rejected:
          type: boolean
          description: |
            Size-gate provenance — set when the measured physical size exceeded
            the label's expected dims beyond the allowance and the detection
            was dropped from the analysis walk. The annotation is KEPT (never
            deleted); cleared when a later run passes it.
        size_reject_detail:
          type: object
          properties:
            exceeded: { type: string, enum: [width, height, area] }
            measured_w_cm: { type: number }
            measured_h_cm: { type: number }
            expected_w_cm: { type: number }
            expected_h_cm: { type: number }
            ratio:
              type: number
              description: measured / (expected × (1 + allowance)) for the tripped check.
    AnnotationGroup:
      type: object
      properties:
        _id: { type: string }
        model_version: { type: string }
        engine:
          type: string
          enum: [trained, zero_shot]
          description: |
            Which detector produced this group: a trained model version
            (default) or a zero-shot VLM (no trained model yet).
        zero_shot_model:
          type: string
          description: "The zero-shot VLM used, e.g. `qwen/qwen3-vl-8b-instruct`."
        usable:
          type: boolean
          default: true
          description: Opt-out training flag.
        time:
          type: number
          description: Epoch ms; defaults to now on create.
        edit_time:
          type: number
          description: Epoch ms; server-stamped on every update.
        annotation_state: { type: string, enum: [auto, manual, auto_edited] }
        confirmed: { type: boolean, default: false }
        confirmed_by:
          $ref: "#/components/schemas/UserRef"
        annotated_by_model_version_code: { type: string }
        annotations:
          type: array
          items:
            $ref: "#/components/schemas/Annotation"
        inference_run:
          type: string
          description: Link to the session's `inference_runs[]._id` (session inference only).
        session_inference:
          type: object
          description: Per-group session inference metadata (session inference only).
          properties:
            plane_normal:
              type: array
              items: { type: number }
            is_winner_in_cluster: { type: boolean }
            cross_frame_overlap_pct: { type: number }
        xai:
          $ref: "#/components/schemas/GroupXai"
    AnnotationGroupCreate:
      description: "An annotation group as sent on create — `annotation_state` is REQUIRED (a group without a valid one is rejected with 400). Server-stamped fields (`confirmed_by`, `edit_time`, `annotated_by_model_version_code`) are ignored."
      allOf:
        - $ref: "#/components/schemas/AnnotationGroup"
        - type: object
          required: [annotation_state]
    GroupXai:
      type: object
      description: |
        Explainable-AI results persisted by the inference service when the
        group was produced with `explain: true`. Heatmap / feature-map images
        live in media storage — each ref is paired with a `*url` publicUrl
        snapshot so consumers can render without a populate round-trip
        (`populatedKeys[]=annotation_groups.xai.heatmap_media` and
        `annotation_groups.xai.feature_maps.media` populate the refs inline).
        `class_scores` / `embeddings.points` reference the lambda predictions
        by `index` (an unmapped prediction never becomes an annotation, so
        they cannot key to `annotations[]`).
      properties:
        parts:
          type: array
          items: { type: string }
          description: Which XAI parts were requested when the group was made.
        heatmap_media:
          type: string
          description: Media ref of the EigenCAM RGBA PNG (alpha = activation).
        heatmap_url:
          type: string
          description: publicUrl snapshot of `heatmap_media`.
        heatmap_method: { type: string, enum: [eigencam] }
        heatmap_width: { type: integer }
        heatmap_height: { type: integer }
        gradcam_media:
          type: string
          description: "Media ref of the TRUE gradient Grad-CAM RGBA PNG (alpha = activation)."
        gradcam_url:
          type: string
          description: publicUrl snapshot of `gradcam_media`.
        gradcam_width: { type: integer }
        gradcam_height: { type: integer }
        gradcam_per_detection:
          type: array
          description: |
            Per-detection Grad-CAM maps ("why THIS object") — one image per
            top-K detection, same overlay contract as `gradcam_media`.
            `index` references the lambda's predictions like `class_scores`.
            Populate the refs with
            `annotation_groups.xai.gradcam_per_detection.media`.
          items:
            type: object
            properties:
              index: { type: integer }
              class: { type: integer }
              name: { type: string }
              confidence: { type: number }
              box:
                type: object
                description: Detection box in original-image pixels (xyxy).
                properties:
                  x1: { type: number }
                  y1: { type: number }
                  x2: { type: number }
                  y2: { type: number }
              match_iou: { type: number }
              media:
                type: string
                description: Media ref of this detection's RGBA PNG map.
              url:
                type: string
                description: publicUrl snapshot of `media`.
              width: { type: integer }
              height: { type: integer }
        feature_maps:
          type: array
          items:
            type: object
            properties:
              stage:
                type: string
                description: "Network stage, e.g. `stage12_C2f`."
              media:
                type: string
                description: Media ref of the stage grid JPEG.
              url:
                type: string
                description: publicUrl snapshot of `media`.
        class_scores:
          type: array
          description: Per-detection top-k class scores (see the inference spec).
          items:
            type: object
            properties:
              index: { type: integer }
              class: { type: integer }
              name: { type: string }
              confidence: { type: number }
              box:
                type: object
                properties:
                  x1: { type: number }
                  y1: { type: number }
                  x2: { type: number }
                  y2: { type: number }
              match_iou: { type: number }
              candidates:
                type: array
                items:
                  type: object
                  properties:
                    class: { type: integer }
                    name: { type: string }
                    score: { type: number }
        embeddings:
          type: object
          description: 2D projection of per-detection embeddings.
          properties:
            method:
              type: string
              enum: [umap, tsne, pca, none]
              description: |
                The projection that actually ran. On `auto` — UMAP for 10+
                detections, t-SNE for 5-9, PCA for 2-4, `none` for a single
                point; explicit requests degrade down the umap -> tsne -> pca
                chain when infeasible.
            requested_method:
              type: string
              enum: [auto, umap, tsne, pca]
              description: "What the caller asked for (differs from `method` on fallback)."
            layer: { type: string }
            embedding_dim: { type: integer }
            points:
              type: array
              items:
                type: object
                properties:
                  index: { type: integer }
                  class: { type: integer }
                  name: { type: string }
                  confidence: { type: number }
                  x: { type: number }
                  y: { type: number }
        confusion_matrix:
          type: object
          description: Training-time confusion-matrix image URLs of the model version.
          properties:
            url: { type: string }
            normalized_url: { type: string }
            source: { type: string, enum: [training_artifacts] }
        notes:
          type: array
          items: { type: string }
          description: Reasons for any XAI part that could not be produced or persisted.
        computed_at:
          type: number
          description: Epoch ms when the XAI results were persisted (withheld when no image could be stored).
    TaskDataset:
      type: object
      required: [dataset, subset]
      properties:
        _id: { type: string }
        dataset: { type: string }
        subset: { type: string, enum: [train, val, test, auto, ignore] }
    FrameMeta:
      type: object
      description: Per-frame AR context, present only on session frames.
      properties:
        frame_id: { type: number }
        ts: { type: number }
        pose:
          type: array
          items: { type: number }
          description: 16-float 4x4 pose matrix, column-major.
        euler_ypr:
          type: array
          items: { type: number }
        intrinsics:
          type: object
          properties:
            fx: { type: number }
            fy: { type: number }
            cx: { type: number }
            cy: { type: number }
        distortion:
          type: object
          properties:
            k1: { type: number }
            k2: { type: number }
            k3: { type: number }
            p1: { type: number }
            p2: { type: number }
        tracking:
          type: object
          properties:
            state: { type: string, enum: [NORMAL, LIMITED, LOST] }
            score: { type: number }
            drift_m: { type: number }
            velocity_mps: { type: number }
        distance_to_shelf_m:
          type: number
          description: Shelf-distance gate used when back-projecting this frame's detections.
        image_rotation_deg:
          type: number
          description: CW degrees the sensor image was rotated to produce the stored image (0/90/180/270).
        image_stats:
          type: object
          properties:
            iso: { type: number }
            shutter: { type: number }
            lux: { type: number }
            sharpness:
              type: number
              description: Variance-of-Laplacian focus measure (higher = sharper).
        depth_summary:
          type: object
          description: Depth-map presence + confidence fractions (election input).
          properties:
            width: { type: number }
            height: { type: number }
            conf_high: { type: number }
            conf_medium: { type: number }
            conf_low: { type: number }
        depth_source: { type: string, enum: [lidar, estimated, none] }
        min_distance_depth:
          type: number
          description: Nearest depth sample in the frame, meters.
        max_distance_depth:
          type: number
          description: Farthest depth sample in the frame, meters.
        depth_variation:
          type: number
          description: Depth span (max - min), meters.
        yaw_degree:
          type: number
          description: Camera yaw at capture, degrees.
        pitch_degree:
          type: number
          description: Camera pitch at capture, degrees.
        roll_degree:
          type: number
          description: Camera roll at capture, degrees.
        frame_sharpness:
          type: number
          description: Variance-of-Laplacian focus measure (higher = sharper; unmeasured frames omit it).
        capture_tier:
          type: string
          enum: [good, warn, error]
          description: Device validator verdict for the frame at capture time.
        detection_settings_polled_at:
          type: number
          description: Epoch ms of the app's last successful detection-settings poll before this frame was submitted.
        app_version:
          type: string
          description: App build that captured the frame.
    TaskSchema:
      type: object
      description: Object detection task document.
      properties:
        _id: { type: string }
        file_media: { type: string }
        shape:
          type: array
          items: { type: number }
        annotation_groups:
          type: array
          items:
            $ref: "#/components/schemas/AnnotationGroup"
        task_dataset:
          type: array
          items:
            $ref: "#/components/schemas/TaskDataset"
        creator:
          $ref: "#/components/schemas/UserRef"
        editor:
          $ref: "#/components/schemas/UserRef"
        annotated: { type: boolean }
        session:
          type: string
          description: Parent session `_id` (session frames only).
        frame_meta:
          $ref: "#/components/schemas/FrameMeta"
        depth_media:
          type: string
          description: Media id of the raw float32 depth blob (session frames only).
        confidence_media:
          type: string
          description: Media id of the raw uint8 confidence blob (session frames only).
        depth_shape:
          type: array
          items: { type: number }
          description: "[width, height] of the depth blob."
        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 }
        file_media_populated:
          type: object
          description: "The `media-storage` document, when `populatedKeys[]=file_media` was requested."
        session_populated:
          type: object
          description: "The `ai-object-detection-session` document, when `populatedKeys[]=session` was requested."
        depth_media_populated:
          type: object
          description: "The `media-storage` document, when `populatedKeys[]=depth_media` was requested."
        confidence_media_populated:
          type: object
          description: "The `media-storage` document, when `populatedKeys[]=confidence_media` was requested."
    TaskCreateBody:
      type: object
      description: |
        Body for creating a task. `creator` is server-injected — do not send
        it. The tenant key (`company_namespace`) is optional for SDK callers
        and is otherwise injected from the caller's session.
      required: [file_media]
      properties:
        file_media: { type: string }
        shape:
          type: array
          items: { type: number }
        annotation_groups:
          type: array
          items:
            $ref: "#/components/schemas/AnnotationGroupCreate"
        task_dataset:
          type: array
          items:
            $ref: "#/components/schemas/TaskDataset"
        session: { type: string }
        frame_meta:
          $ref: "#/components/schemas/FrameMeta"
        depth_media: { type: string }
        confidence_media: { type: string }
        depth_shape:
          type: array
          items: { type: number }
        company_namespace:
          type: array
          items: { type: string }
          description: Optional tenant namespace override for SDK callers.
    TaskUpdateBody:
      type: object
      description: |
        Body for updating a task. `editor` is server-injected and `annotated`
        re-derived. `annotation_groups` replaces the stored array wholesale.
        Set `disabled: true` to soft-delete.
      properties:
        file_media: { type: string }
        shape:
          type: array
          items: { type: number }
        annotation_groups:
          type: array
          items:
            $ref: "#/components/schemas/AnnotationGroup"
        task_dataset:
          type: array
          items:
            $ref: "#/components/schemas/TaskDataset"
        session: { type: string }
        frame_meta:
          $ref: "#/components/schemas/FrameMeta"
        depth_media: { type: string }
        confidence_media: { type: string }
        depth_shape:
          type: array
          items: { type: number }
        disabled: { type: boolean }
    PatchActionWrite:
      type: object
      required: [key, command, value]
      properties:
        key:
          type: string
          description: "Dotted document path, e.g. `annotation_groups.0.confirmed`."
        command:
          type: string
          enum: [set, addToSet, pull]
          description: "`set` → `$set`; `addToSet` → `$addToSet: { $each: value }`; `pull` → `$pull: { $in: value }`."
        value: {}
    PatchActionBody:
      type: object
      description: Bulk-update body. `writeQuery[]` describes the writes applied to filtered rows.
      required: [writeQuery]
      properties:
        writeQuery:
          type: array
          items:
            $ref: "#/components/schemas/PatchActionWrite"
    PatchActionResult:
      type: object
      properties:
        nFound:
          type: number
          description: Matched documents.
        nModified:
          type: number
          description: Modified documents.
    TaskFindResult:
      type: object
      description: Standard paginated result envelope.
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/TaskSchema"
        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 }
