openapi: "3.1.0"

info:
  title: "Zenith — Knowledge Engine API"
  version: "0.1.0"
  description: |
    Zenith is the APEX Suite knowledge engine. Ingest a document corpus once, then query it:
    retrieval-augmented answers with source citations and claim verification (`rag` mode),
    structured JSON extraction (`extract` mode), scored checklist matching (`match` mode), and
    retrieval-only semantic search. Every answer carries token usage and a cost breakdown in
    South African Rand. Authentication is a single `Authorization: Bearer` header carrying either
    a machine-to-machine access token or a Forge-issued tenant API key. Your tenant identifier is
    the first path segment of every tenant-scoped route and must match the tenant your credential
    is scoped to — a mismatch is rejected with `403`.

servers:
  - url: https://zenith.dev.apex.reisiger.org
    description: Development
  - url: http://localhost:9100
    description: Local

security:
  - bearerAuth: []

tags:
  - name: Query
    description: >-
      Retrieval-augmented question answering, structured extraction, and checklist matching
      against your ingested corpus.
  - name: Search
    description: >-
      Retrieval-only semantic search. No language model is invoked and no generation cost is
      incurred.
  - name: Ingestion
    description: >-
      Asynchronous corpus ingestion — start a run, then poll its job until it completes.
  - name: Ingestion Runs
    description: >-
      Durable ingestion runs — start, list, watch, cancel, and resume corpus-ingestion and
      knowledge-graph jobs. Unlike legacy ingestion jobs, runs are persisted and survive
      service restarts.
  - name: Continuous Sync
    description: >-
      Keep the corpus continuously aligned with its sources — inspect per-source sync
      checkpoints and manage per-source sync schedules.
  - name: Failed Documents
    description: >-
      Remediation queue for documents that failed ingestion — review open failures, dismiss
      them with an audit note, reopen them, or upload a replacement file.
  - name: Corpus Browse
    description: >-
      Read-only views over the ingested corpus — aggregate statistics, full ingestion
      accounting, and paged document and passage listings.
  - name: Knowledge Graph
    description: >-
      Read-only views over the knowledge graph extracted from your corpus — entities,
      relations, communities, and the Knowledge-Graph Atlas. Tenants without knowledge-graph
      data receive empty results, not errors.
  - name: Configuration
    description: Read-only view of your tenant's knowledge-engine configuration.
  - name: Service
    description: Service liveness.

paths:
  /api/v1/{tenant_id}/query:
    post:
      operationId: queryKnowledgeBase
      tags: [Query]
      summary: Run a query against your knowledge base
      x-apex-availability: available
      description: |
        Runs one query against your ingested corpus and returns a generated answer with source
        citations, optional claim verification, token usage, and a cost breakdown in South African
        Rand.

        Three modes are supported:
        - `rag` (default) — a prose answer grounded in retrieved passages, with citations.
        - `extract` — structured JSON output, optionally shaped by `output_schema`.
        - `match` — the corpus is scored against each entry in `match_criteria`.

        Service behaviour: requests are throttled per tenant (default 120 requests per minute) and
        per credential (default 60 requests per minute); rejected requests return `429` with a
        `Retry-After` header. Each request has a 30-second processing budget; exceeding it returns
        `504`. Successful responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/QueryRequest"
            examples:
              retrievalAugmentedAnswer:
                summary: Default mode — grounded prose answer with citations
                value:
                  query: "What inspection steps does our maintenance policy require before a pressure test is signed off?"
              structuredExtraction:
                summary: Extract mode — structured JSON output
                value:
                  query: "Extract the commissioning milestones and their target dates from the project handover documentation."
                  mode: extract
                  output_schema:
                    type: object
                    properties:
                      milestones:
                        type: array
                        items:
                          type: object
                          properties:
                            name:
                              type: string
                            target_date:
                              type: string
              checklistMatch:
                summary: Match mode — score the corpus against a checklist
                value:
                  query: "Assess the supplier submission pack against our compliance checklist."
                  mode: match
                  match_criteria:
                    - "Valid quality-management certification on file"
                    - "Signed health and safety plan included"
                    - "Local content declaration present"
      responses:
        "200":
          description: Answer envelope — generated answer plus cost breakdown.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QueryResponseEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/batch-query:
    post:
      operationId: batchQueryKnowledgeBase
      tags: [Query]
      summary: Run multiple queries in parallel
      x-apex-availability: available
      description: |
        Submits between 1 and 50 queries in a single request. Queries run in parallel; each item
        supports the full mode set (`rag`, `extract`, `match`).

        Per-item failures do not fail the request: the batch always returns `200`, and a failed
        item is returned as a normal envelope whose `data.answer` begins with `"Error: ..."`,
        `data.pipeline_used` is `"error"`, and whose cost is zero. `total_cost` aggregates the
        cost of every item in South African Rand.

        Service behaviour: the batch runs within a single HTTP request, and each item runs under
        a per-item processing budget (25 seconds by default). An item that exceeds the budget is
        returned as an error item ("query exceeded the batch budget"); the batch still returns
        `200` with every item that completed. Prefer short queries or `extract`-mode queries in
        batches, and route long-form generations through the single-query endpoint instead. The
        batch counts as a single request against the rate limits (default 120 requests per minute
        per tenant, 60 per credential). Size batches accordingly.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchQueryRequest"
            examples:
              parallelQueries:
                summary: Three queries in one call
                value:
                  queries:
                    - query: "Summarise the warranty terms for rotating equipment."
                    - query: "Which standards govern welding procedure qualification?"
                    - query: "Score the handover file against our completion checklist."
                      mode: match
                      match_criteria:
                        - "As-built drawings delivered"
                        - "Operating manuals delivered"
      responses:
        "200":
          description: >-
            Aggregated batch results. Always `200` when the batch itself is accepted; inspect each
            item's `data.pipeline_used` for the value `"error"` to detect per-item failures.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchQueryResponseEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/search:
    get:
      operationId: searchKnowledgeBase
      tags: [Search]
      summary: Search your knowledge base (retrieval only)
      x-apex-availability: available
      description: |
        Semantic search over your ingested corpus. Returns ranked passages with scores and
        metadata. No language model is invoked, no answer is generated, and no generation cost is
        incurred — use this when you want raw passages rather than a synthesised answer.

        Service behaviour: throttled per tenant (default 120 requests per minute) and per
        credential (default 60 per minute) with `429` + `Retry-After` on rejection; 30-second
        processing budget with `504` on expiry. Successful responses carry `X-RateLimit-Limit`
        and `X-RateLimit-Remaining` headers.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: q
          in: query
          required: true
          description: The search text.
          schema:
            type: string
            minLength: 1
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
      responses:
        "200":
          description: Ranked search results.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest:
    post:
      operationId: startIngestionRun
      tags: [Ingestion]
      summary: Start an ingestion run (asynchronous)
      x-apex-availability: available
      description: |
        Starts an ingestion run over your tenant's configured document sources and returns `202`
        immediately with a job record. The pipeline itself runs in the background — poll
        `GET /api/v1/{tenant_id}/ingest/{job_id}` for progress and the final reconciliation
        report.

        The request body fields `source` and `priority` are accepted, but the current release
        always runs the tenant's full configured source set — source-scoped and prioritised runs
        are not yet honoured.

        Service behaviour: the submission call has a 300-second budget (the background run is not
        subject to it). Rate limits apply as on all tenant routes (default 120 requests per minute
        per tenant, 60 per credential). Job status is retained in service memory and is not
        durable across service restarts — see the polling endpoint for the resulting `404`
        semantics.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IngestRequest"
            examples:
              fullRun:
                summary: Start a run over the full configured source set
                value:
                  priority: normal
      responses:
        "202":
          description: >-
            Ingestion run accepted. The returned job starts in status `queued`; poll the job
            endpoint with `job_id` for progress.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestJob"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/{job_id}:
    get:
      operationId: getIngestionJob
      tags: [Ingestion]
      summary: Poll an ingestion job
      x-apex-availability: available
      description: |
        Returns the current state of an ingestion job started via the ingest endpoint. Status
        transitions are `queued` → `running` → `completed` or `failed`. While running,
        `current_stage` carries a live progress string; on completion, `results` holds per-stage
        metrics and `ingestion_report` holds the full reconciliation (every input document
        accounted for as indexed, duplicate, or parked, plus the run's total cost in South
        African Rand).

        A `404` is returned both for job identifiers that do not exist and for jobs belonging to
        a different tenant — the two cases are deliberately indistinguishable. Job status is held
        in service memory, so a service restart clears job history; treat a `404` for a job
        identifier you previously received as "status no longer available", not as proof the run
        never happened.

        Service behaviour: standard rate limits and the 30-second request budget apply.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: job_id
          in: path
          required: true
          description: Job identifier returned by the ingest submission call.
          schema:
            type: string
      responses:
        "200":
          description: Current job state.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestJob"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: >-
            Unknown job identifier, a job belonging to another tenant, or job history cleared by
            a service restart. All cases return the same response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Job 'a1b2c3d4e5f6' not found"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/runs:
    post:
      operationId: startDurableIngestionRun
      tags: [Ingestion Runs]
      summary: Start a durable ingestion run
      x-apex-availability: available
      description: |
        Starts a durable ingestion run and returns `202` with the run record. Unlike the legacy
        ingest endpoint, runs are persisted: they survive service restarts, can be cancelled and
        resumed, and expose live progress via the run's `progress` endpoint.

        The run can be scoped to specific configured sources (`source_ids`), forced to re-process
        unchanged content (`force_reingest`), restricted to an incremental delta pass
        (`incremental`), and split into parallel lanes. Lanes are defined either explicitly with
        `shard_spec` (one lane per inner array of path prefixes) or automatically by setting
        `max_concurrent_lanes` above 1 without a `shard_spec`.

        A run that was in flight when the service restarted is later reported as `interrupted`
        rather than `running`, and can be resumed.

        Service behaviour: standard rate limits apply. The submission returns immediately; the
        run itself executes in the background.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StartIngestionRunRequest"
            examples:
              fullRun:
                summary: Full run over all configured sources
                value: {}
              scopedShardedRun:
                summary: One source, two prefix lanes, forced re-ingest
                value:
                  source_ids: [document-library]
                  shard_spec:
                    - ["Projects/Alpha"]
                    - ["Projects/Beta", "Projects/Gamma"]
                  force_reingest: true
                  max_concurrent_lanes: 2
      responses:
        "202":
          description: Run accepted and persisted in status `queued`.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestionRun"
              example:
                $ref: "#/components/examples/ingestionRunQueued/value"
        "400":
          description: Unknown source identifier or invalid shard specification.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Unknown source_id 'archive-2019' for tenant 'acme'"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
    get:
      operationId: listIngestionRuns
      tags: [Ingestion Runs]
      summary: List ingestion runs
      x-apex-availability: available
      description: |
        Pages through the tenant's ingestion and knowledge-graph runs, newest first. Runs whose
        execution was lost to a service restart are reported as `interrupted` (resumable) rather
        than left `running` forever.

        Service behaviour: standard rate limits and the 30-second request budget apply.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: limit
          in: query
          required: false
          description: Maximum number of runs to return.
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of runs to skip.
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Page of runs, newest first.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestionRunList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/runs/kg-trailing:
    post:
      operationId: enqueueKnowledgeGraphExtraction
      tags: [Ingestion Runs]
      summary: Enqueue the knowledge-graph extraction pass
      x-apex-availability: available
      description: |
        Queues a `kg_trailing` run: entities and relations are extracted for stored documents
        that do not yet have knowledge-graph rows, and communities are re-detected afterwards.
        The pass is idempotent — already-extracted documents are skipped — so it is safe to
        enqueue after any ingestion run.

        Requires knowledge-graph features to be configured for your tenant; without them the
        request is rejected with `400`. Knowledge-graph jobs are mutually exclusive per tenant:
        at most one extraction or refinement job may be queued or running at a time, and repeat
        calls while either is active return `409`.

        Track the job through the standard runs endpoints using the returned `run_id`.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      responses:
        "202":
          description: Knowledge-graph extraction job queued.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestionRun"
              example:
                $ref: "#/components/examples/knowledgeGraphRunQueued/value"
        "400":
          description: The tenant has no knowledge-graph configuration.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "tenant config has no knowledge_graph section"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "409":
          description: A knowledge-graph job is already queued or running for this tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "a knowledge-graph trailing job is already queued or running for this tenant"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/runs/kg-refine:
    post:
      operationId: enqueueKnowledgeGraphRefinement
      tags: [Ingestion Runs]
      summary: Enqueue a knowledge-graph refinement pass
      x-apex-availability: available
      description: |
        Queues a `kg_refine` run over the live knowledge graph. The refinement pass merges
        duplicate entities that share a normalised name, flags document-artifact entities so
        they no longer pollute graph views, and re-detects communities over the cleaned graph.

        Requires knowledge-graph features to be configured for your tenant; without them the
        request is rejected with `400`. Knowledge-graph jobs are mutually exclusive per tenant:
        at most one refinement or extraction job may be queued or running at a time, and repeat
        calls while either is active return `409`.

        Track the job through the standard runs endpoints using the returned `run_id`.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      responses:
        "202":
          description: Knowledge-graph refinement job queued.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestionRun"
              example:
                run_id: "5e8f0a2b6c3d"
                tenant_id: acme
                kind: kg_refine
                status: queued
                source_ids: []
                shard_spec: null
                params:
                  force_reingest: false
                  incremental: null
                  max_concurrent_lanes: 1
                current_stage: ""
                lanes_total: 1
                lane_state: {}
                counters: {}
                report: null
                error: null
                started_by: api
                claimed_by: null
                cancel_requested: false
                created_at: "2026-08-18T09:02:11Z"
                started_at: null
                finished_at: null
                heartbeat_at: null
                claimed_at: null
        "400":
          description: The tenant has no knowledge-graph configuration.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "tenant config has no knowledge_graph section"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "409":
          description: A knowledge-graph job is already queued or running for this tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "a knowledge-graph job is already queued or running for this tenant"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/runs/{run_id}:
    get:
      operationId: getIngestionRun
      tags: [Ingestion Runs]
      summary: Get one ingestion run
      x-apex-availability: available
      description: |
        Returns the persisted state of a run. On completion, `report` carries the full
        reconciliation (every input document accounted for as indexed, duplicate, or parked, the
        run's total cost in South African Rand, and `report.stats` with a file-type breakdown,
        page-image text-recognition counts, per-stage timings, and a failure-reason table).

        Because runs are persisted, a `run_id` remains queryable after service restarts —
        unlike legacy ingestion job identifiers.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/RunId"
      responses:
        "200":
          description: The run record.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestionRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Unknown run identifier, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Run '0f3a1b2c4d5e' not found"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/runs/{run_id}/cancel:
    post:
      operationId: cancelIngestionRun
      tags: [Ingestion Runs]
      summary: Cancel an active ingestion run
      x-apex-availability: available
      description: |
        Requests cancellation of a `queued` or `running` run. Cancellation is cooperative: the
        run stops at its next progress checkpoint, so the returned record may still read
        `running` with `cancel_requested: true` for a short while before settling on
        `cancelled`. A cancelled run can be resumed later — completed work is not repeated.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/RunId"
      responses:
        "200":
          description: Cancellation recorded; the current run record is returned.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestionRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Unknown run identifier, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Run '0f3a1b2c4d5e' not found"
        "409":
          description: The run is not active (already completed, failed, cancelled, or interrupted).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Run '0f3a1b2c4d5e' is 'completed' — cannot cancel"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/runs/{run_id}/resume:
    post:
      operationId: resumeIngestionRun
      tags: [Ingestion Runs]
      summary: Resume an interrupted, failed, or cancelled run
      x-apex-availability: available
      description: |
        Re-queues a run whose status is `interrupted`, `failed`, or `cancelled`. Documents that
        already reached a terminal outcome in earlier attempts are skipped, so resuming never
        double-processes content. Runs in any other status are rejected with `409`.

        For knowledge-graph runs, resuming while another job of the same kind is queued or
        running also returns `409`.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/RunId"
      responses:
        "202":
          description: Run re-queued; the refreshed run record is returned.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestionRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Unknown run identifier, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Run '0f3a1b2c4d5e' not found"
        "409":
          description: >-
            The run is not resumable (only `interrupted`, `failed`, or `cancelled` runs can be
            resumed), or a knowledge-graph job of the same kind is already active.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "run '0f3a1b2c4d5e' is 'running' — only interrupted/failed/cancelled runs can be resumed"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/runs/{run_id}/progress:
    get:
      operationId: getIngestionRunProgress
      tags: [Ingestion Runs]
      summary: Live progress for an ingestion run
      x-apex-availability: available
      description: |
        Returns a live progress view of a run: the per-stage document funnel, windowed
        throughput rates, a gauge for page-image text recognition, and an `expected_total` /
        `processed` / `eta_seconds` estimate. `owned` indicates whether the responding service
        instance is executing the run — when `false`, rates and the recognition gauge may be
        slightly stale or absent and the counters are historical.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/RunId"
      responses:
        "200":
          description: Live progress snapshot.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestionRunProgress"
              example:
                run_id: "0f3a1b2c4d5e"
                status: running
                current_stage: "parser_fleet: running (lane 0)"
                lanes_total: 2
                lanes_done: 0
                lanes:
                  "0": {lane: 0, prefixes: ["Projects/Alpha"], status: running, current_stage: "parser_fleet", counters: {}, error: null}
                  "1": {lane: 1, prefixes: ["Projects/Beta"], status: queued, current_stage: "", counters: {}, error: null}
                stages:
                  source_adapters: {records_out: 512}
                  parser_fleet: {records_out: 210}
                rates:
                  stages:
                    parser_fleet: {records_out_per_min: 42.0}
                ocr: {queued: 3, in_flight: 2, completed_pages: 118}
                expected_total: 512
                processed: 210
                eta_seconds: 431.4
                owned: true
                claimed_by: "runner-1"
                heartbeat_at: "2026-08-18T09:14:02Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Unknown run identifier, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Run '0f3a1b2c4d5e' not found"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/sync-state:
    get:
      operationId: listSourceSyncState
      tags: [Continuous Sync]
      summary: List per-source sync checkpoints
      x-apex-availability: available
      description: |
        Returns the incremental-sync checkpoint for every source that has synced at least once:
        whether a delta checkpoint is held (`has_delta_token`), the last run that advanced it,
        when the source last synced, and how many items the last pass discovered or failed to
        fetch. A source with a delta checkpoint only re-crawls what changed; without one, the
        next run performs a full crawl.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      responses:
        "200":
          description: Checkpoint rows, one per synced source.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SyncStateList"
              example:
                sources:
                  - source_id: document-library
                    has_delta_token: true
                    last_run_id: "0f3a1b2c4d5e"
                    last_synced_at: "2026-08-18T06:00:41Z"
                    last_discovered: 37
                    last_fetch_errors: 0
                    updated_at: "2026-08-18T06:00:41Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/sync-state/{source_id}/reset:
    post:
      operationId: resetSourceSyncState
      tags: [Continuous Sync]
      summary: Clear a source's delta checkpoint
      x-apex-availability: available
      description: |
        Clears the incremental-sync checkpoint for one source so that the next incremental run
        performs a full crawl instead of a delta pass. Use this when a source's checkpoint has
        been invalidated upstream or when you suspect drift between the source and the corpus.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/SourceId"
      responses:
        "200":
          description: Checkpoint cleared.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                type: object
                required: [source_id, delta_token_cleared]
                properties:
                  source_id:
                    type: string
                  delta_token_cleared:
                    type: boolean
              example:
                source_id: document-library
                delta_token_cleared: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No sync state exists for that source, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "No sync state for source 'document-library'"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/sync-schedule:
    get:
      operationId: listSyncSchedules
      tags: [Continuous Sync]
      summary: List sync schedules
      x-apex-availability: available
      description: |
        Returns the continuous-sync schedule for every source that has one. Each schedule
        enqueues an incremental ingestion run whenever its `next_due_at` passes — those runs are
        ordinary durable runs, auditable through the runs endpoints via
        `last_enqueued_run_id`. While an ingestion run is already active for the tenant, due
        schedules are skipped rather than stacked.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      responses:
        "200":
          description: All schedules for the tenant.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SyncScheduleList"
              example:
                schedules:
                  - source_id: document-library
                    enabled: true
                    paused: false
                    interval_minutes: 240
                    scope_prefixes: null
                    last_enqueued_run_id: "0f3a1b2c4d5e"
                    last_enqueued_at: "2026-08-18T06:00:00+00:00"
                    next_due_at: "2026-08-18T10:00:00+00:00"
                    updated_by: "ops@acme"
                    updated_at: "2026-08-17T15:21:09+00:00"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/sync-schedule/{source_id}:
    put:
      operationId: upsertSyncSchedule
      tags: [Continuous Sync]
      summary: Create or update a source's sync schedule
      x-apex-availability: available
      description: |
        Creates or replaces the sync schedule for one configured source. Set the cadence with
        `interval_minutes` (5 minutes to 7 days) and optionally scope the schedule to a subtree
        with `scope_prefixes` — scoped runs re-crawl just those prefixes and never advance the
        source-wide delta checkpoint, so full-source incremental syncs stay correct. After an
        update the next run is due immediately.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/SourceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SyncScheduleRequest"
            examples:
              everyFourHours:
                summary: Full-source sync every four hours
                value:
                  interval_minutes: 240
              scopedDaily:
                summary: Daily sync of one subtree
                value:
                  interval_minutes: 1440
                  scope_prefixes: ["Projects/Alpha"]
      responses:
        "200":
          description: The stored schedule.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SyncSchedule"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The source is not configured for this tenant, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "No source adapter 'document-library' in tenant config"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
    delete:
      operationId: deleteSyncSchedule
      tags: [Continuous Sync]
      summary: Remove a source's sync schedule
      x-apex-availability: available
      description: |
        Deletes the sync schedule for one source. The source's checkpoint and corpus content are
        untouched — only the automatic cadence stops.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/SourceId"
      responses:
        "200":
          description: Schedule removed.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                type: object
                required: [source_id, deleted]
                properties:
                  source_id:
                    type: string
                  deleted:
                    type: boolean
              example:
                source_id: document-library
                deleted: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No schedule exists for that source, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "No schedule for that source"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/sync-schedule/{source_id}/pause:
    post:
      operationId: pauseSyncSchedule
      tags: [Continuous Sync]
      summary: Pause a sync schedule
      x-apex-availability: available
      description: |
        Pauses the schedule: it is kept, but no runs are enqueued until it is resumed.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/SourceId"
      responses:
        "200":
          description: The updated schedule with `paused` set to `true`.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SyncSchedule"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No schedule exists for that source, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "No schedule for that source"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/sync-schedule/{source_id}/resume:
    post:
      operationId: resumeSyncSchedule
      tags: [Continuous Sync]
      summary: Resume a paused sync schedule
      x-apex-availability: available
      description: |
        Resumes a paused schedule; enqueueing continues from its existing cadence.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/SourceId"
      responses:
        "200":
          description: The updated schedule with `paused` set to `false`.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SyncSchedule"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No schedule exists for that source, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "No schedule for that source"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/sync-schedule/{source_id}/sync-now:
    post:
      operationId: syncSourceNow
      tags: [Continuous Sync]
      summary: Enqueue an immediate sync of a source
      x-apex-availability: available
      description: |
        Enqueues a durable ingestion run for one source right now, without waiting for its
        schedule (a schedule is not required). Provide `prefixes` to sync just a subtree —
        scoped runs re-crawl those prefixes and never advance the source-wide delta checkpoint.
        Omit `prefixes` for a full incremental delta sync of the source.

        The returned run is trackable through the standard runs endpoints.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/SourceId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SyncNowRequest"
            examples:
              subtree:
                summary: Sync one subtree immediately
                value:
                  prefixes: ["Projects/Alpha/Reports"]
              fullDelta:
                summary: Full incremental delta sync
                value: {}
      responses:
        "202":
          description: Sync run enqueued in status `queued`.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestionRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The source is not configured for this tenant, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "No source adapter 'document-library' in tenant config"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/failed-docs:
    get:
      operationId: listFailedDocuments
      tags: [Failed Documents]
      summary: List the failed-document queue
      x-apex-availability: available
      description: |
        Pages through documents that failed ingestion (parked records), most recently updated
        first, with aggregate breakdowns by stage, reason, and source. The summary always covers
        the full filtered population, not just the returned page, so its totals reconcile
        against your corpus accounting.

        By default only open failures are returned; set `include_resolved=true` to include
        dismissed and replaced records. Set `format=csv` to download the page as CSV instead of
        JSON — useful for review outside the API.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: source_id
          in: query
          required: false
          description: Only failures from this source.
          schema:
            type: string
        - name: stage
          in: query
          required: false
          description: Only failures that occurred in this pipeline stage.
          schema:
            type: string
        - name: include_resolved
          in: query
          required: false
          description: Include dismissed and replaced records alongside open failures.
          schema:
            type: boolean
            default: false
        - name: limit
          in: query
          required: false
          description: Maximum number of records to return.
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
        - name: offset
          in: query
          required: false
          description: Number of records to skip.
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: format
          in: query
          required: false
          description: Response format — JSON (default) or CSV of the returned page.
          schema:
            type: string
            enum: [json, csv]
            default: json
      responses:
        "200":
          description: The failed-document queue (JSON) or a CSV rendering of the page.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FailedDocumentQueue"
              example:
                items:
                  - raw_hash: "9f2b1c0d8e7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c"
                    file_name: "site-survey-scan.pdf"
                    file_path: "Projects/Alpha/site-survey-scan.pdf"
                    source_id: document-library
                    status: parked
                    reason: "no text extracted"
                    stage: parser_fleet
                    run_id: "0f3a1b2c4d5e"
                    resolution: ""
                    resolution_note: ""
                    resolved_by: ""
                    resolved_at: null
                    replacement_document_id: null
                    updated_at: "2026-08-18T08:17:33+00:00"
                total: 1
                limit: 100
                offset: 0
                summary:
                  open: 1
                  dismissed: 4
                  replaced: 2
                  by_stage: {parser_fleet: 1}
                  by_reason: {"no text extracted": 1}
                  by_source: {document-library: 1}
            text/csv:
              schema:
                type: string
              example: |
                file_name,file_path,source_id,stage,reason,resolution,resolution_note,run_id,updated_at
                site-survey-scan.pdf,Projects/Alpha/site-survey-scan.pdf,document-library,parser_fleet,no text extracted,,,0f3a1b2c4d5e,2026-08-18T08:17:33+00:00
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/failed-docs/{raw_hash}/dismiss:
    post:
      operationId: dismissFailedDocument
      tags: [Failed Documents]
      summary: Dismiss a failed document with an audit note
      x-apex-availability: available
      description: |
        Accepts a failure: the record leaves the open queue with resolution `dismissed` and your
        note is kept for audit. Dismissed records count toward "accounted" in the corpus
        verification totals. A dismissal can be undone with the reopen endpoint.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/RawHash"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DismissFailedDocumentRequest"
            example:
              note: "Blank scan of a superseded drawing — no content to recover."
      responses:
        "200":
          description: Record dismissed.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FailedDocumentResolution"
              example:
                raw_hash: "9f2b1c0d8e7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c"
                resolution: dismissed
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No open failed document with that hash, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "No parked document with that hash"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/failed-docs/{raw_hash}/reopen:
    post:
      operationId: reopenFailedDocument
      tags: [Failed Documents]
      summary: Reopen a dismissed or replaced record
      x-apex-availability: available
      description: |
        Undoes a dismissal or replacement: the record returns to the open queue with an empty
        resolution, ready to be remediated again.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/RawHash"
      responses:
        "200":
          description: Record reopened.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FailedDocumentResolution"
              example:
                raw_hash: "9f2b1c0d8e7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c"
                resolution: ""
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No resolved failed document with that hash, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "No resolved parked document with that hash"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/ingest/failed-docs/{raw_hash}/replace:
    post:
      operationId: replaceFailedDocument
      tags: [Failed Documents]
      summary: Upload a replacement file for a failed document
      x-apex-availability: available
      description: |
        Uploads a replacement file for a failed document and ingests it through the full
        pipeline as a single-document run bound to the original document's source identity, so
        the replacement inherits the original's source and path. Returns `202` with a job
        record — poll `GET /api/v1/{tenant_id}/ingest/{job_id}` for progress. The failed record
        resolves to `replaced` only when the replacement actually indexes (a duplicate of
        already-indexed content also counts); otherwise the job reports `failed` and the record
        stays open.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - $ref: "#/components/parameters/RawHash"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: The replacement file content.
                note:
                  type: string
                  default: ""
                  description: Optional audit note recorded on the resolution.
      responses:
        "202":
          description: Replacement ingest accepted; poll the returned job for the outcome.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestJob"
              example:
                job_id: "b4c5d6e7f8a9"
                tenant_id: acme
                status: queued
                created_at: "2026-08-18T09:30:02Z"
                current_stage: ""
                results: null
                ingestion_report: null
                elapsed_ms: null
                error: null
                cost: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No open failed document with that hash, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "No parked document with that hash"
        "422":
          description: Validation failed — for example, the uploaded file is empty.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Uploaded file is empty"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/corpus/stats:
    get:
      operationId: getCorpusStats
      tags: [Corpus Browse]
      summary: Corpus statistics
      x-apex-availability: available
      description: |
        Aggregate statistics for the ingested corpus: document, duplicate, passage, token, and
        byte totals, the most recent ingestion time, and breakdowns by file type and by source.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      responses:
        "200":
          description: Corpus totals and breakdowns.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CorpusStats"
              example:
                documents: 1042
                duplicates: 118
                total_bytes: 5488230144
                chunks: 19334
                tokens: 6120458
                last_ingested_at: "2026-08-18T06:00:41+00:00"
                by_file_type:
                  - {file_type: pdf, count: 611}
                  - {file_type: docx, count: 302}
                by_source:
                  - {source_id: document-library, count: 1042, bytes: 5488230144}
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/corpus/verification:
    get:
      operationId: getCorpusVerification
      tags: [Corpus Browse]
      summary: Corpus ingestion accounting
      x-apex-availability: available
      description: |
        Full-corpus accounting from discovery through to retrievability, with an overall
        `green`/`amber`/`red` status. Three views are combined:

        - **Totals** — every discovered document accounted for as indexed, duplicate, open
          failure, dismissed, or replaced, with an accounted percentage (overall and per source).
        - **Pipeline chain** — stored documents → documents with passages → documents fully
          retrievable by search.
        - **Integrity checks** — named checks (each `ok`, `warn`, or `fail`) covering open
          failures, accounting rows whose document is missing, documents stored without
          passages, and passages stored without an embedding.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      responses:
        "200":
          description: Accounting rollup, pipeline chain, and integrity checks.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CorpusVerification"
              example:
                status: amber
                totals:
                  discovered: 1060
                  indexed: 1042
                  duplicate: 12
                  parked_open: 1
                  dismissed: 4
                  replaced: 1
                  accounted: 1059
                  accounted_pct: 99.91
                pipeline:
                  documents: 924
                  duplicate_documents: 118
                  chunked_documents: 924
                  retrievable_documents: 924
                  chunks: 19334
                  embedded_chunks: 19334
                checks:
                  - {id: open_parked, label: "Parked documents awaiting remediation", status: warn, count: 1}
                  - {id: orphaned_ledger, label: "Ledger rows marked indexed but document missing", status: ok, count: 0}
                  - {id: chunkless_documents, label: "Documents stored with zero chunks", status: ok, count: 0}
                  - {id: unembedded_chunks, label: "Chunks stored without an embedding", status: ok, count: 0}
                by_source:
                  - source_id: document-library
                    discovered: 1060
                    indexed: 1042
                    duplicate: 12
                    parked_open: 1
                    dismissed: 4
                    replaced: 1
                    accounted: 1059
                    accounted_pct: 99.91
                generated_at: "2026-08-18T09:31:44+00:00"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/corpus/documents:
    get:
      operationId: listCorpusDocuments
      tags: [Corpus Browse]
      summary: List ingested documents
      x-apex-availability: available
      description: |
        Pages through ingested documents, newest first, each with its passage count. Filter by
        source and/or search file names and paths with `q` (case-insensitive substring match).
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: limit
          in: query
          required: false
          description: Maximum number of documents to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of documents to skip.
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: source_id
          in: query
          required: false
          description: Only documents from this source.
          schema:
            type: string
            maxLength: 64
        - name: q
          in: query
          required: false
          description: Case-insensitive substring match on file name or source path.
          schema:
            type: string
            maxLength: 200
      responses:
        "200":
          description: Page of documents, newest first.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CorpusDocumentList"
              example:
                total: 1042
                limit: 50
                offset: 0
                documents:
                  - document_id: "3f7c2a90-51b4-4e2a-9c0d-8a1b2c3d4e5f"
                    source_id: document-library
                    source_path: "Projects/Alpha/handover-pack.pdf"
                    file_name: "handover-pack.pdf"
                    file_type: pdf
                    content_type: file
                    file_size_bytes: 2418277
                    is_duplicate: false
                    ingested_at: "2026-08-18T06:00:12+00:00"
                    chunk_count: 44
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/corpus/documents/{document_id}/chunks:
    get:
      operationId: listDocumentChunks
      tags: [Corpus Browse]
      summary: List a document's passages
      x-apex-availability: available
      description: |
        Returns one document's metadata and its passages in document order — the exact text
        units that retrieval and citations operate on.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: document_id
          in: path
          required: true
          description: Document identifier (as returned by the document listing).
          schema:
            type: string
            pattern: "^[0-9a-fA-F-]{36}$"
        - name: limit
          in: query
          required: false
          description: Maximum number of passages to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of passages to skip.
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Document metadata plus ordered passages.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentChunkList"
              example:
                document:
                  document_id: "3f7c2a90-51b4-4e2a-9c0d-8a1b2c3d4e5f"
                  source_id: document-library
                  source_path: "Projects/Alpha/handover-pack.pdf"
                  file_name: "handover-pack.pdf"
                  file_type: pdf
                  content_type: file
                  file_size_bytes: 2418277
                  is_duplicate: false
                  ingested_at: "2026-08-18T06:00:12+00:00"
                total: 44
                limit: 50
                offset: 0
                chunks:
                  - chunk_id: "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"
                    chunk_index: 0
                    content: "Section 1 — Scope of handover. This pack covers ..."
                    token_count: 412
                    chunk_strategy: recursive
                    created_at: "2026-08-18T06:00:13+00:00"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Unknown document identifier, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Document '3f7c2a90-51b4-4e2a-9c0d-8a1b2c3d4e5f' not found"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/graph/stats:
    get:
      operationId: getGraphStats
      tags: [Knowledge Graph]
      summary: Knowledge-graph health and distributions
      x-apex-availability: available
      description: |
        Health metrics and distributions for the tenant knowledge graph: live and retired
        entity/relation counts, source-document coverage, average extraction confidence, the
        entity-type and predicate distributions, and the community count. Tenants without
        knowledge-graph data receive zeroed/empty values.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      responses:
        "200":
          description: Graph health metrics and distributions.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GraphStats"
              example:
                live_entities: 4211
                tombstoned_entities: 302
                live_triples: 9877
                tombstoned_triples: 641
                source_documents: 812
                entity_types: 14
                avg_entity_confidence: 0.81
                avg_triple_confidence: 0.77
                by_entity_type:
                  - {entity_type: organization, count: 1211}
                  - {entity_type: standard, count: 604}
                by_predicate:
                  - {predicate: governed_by, count: 1420}
                communities: 37
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/graph/entities:
    get:
      operationId: listGraphEntities
      tags: [Knowledge Graph]
      summary: List knowledge-graph entities
      x-apex-availability: available
      description: |
        Pages through live entities, highest extraction confidence first. Filter by entity type,
        search names with `q` (case-insensitive substring match), or pass `document_id` to see
        exactly which entities were extracted from one document — the corpus-to-graph
        back-trace.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: limit
          in: query
          required: false
          description: Maximum number of entities to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 100
        - name: offset
          in: query
          required: false
          description: Number of entities to skip.
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: entity_type
          in: query
          required: false
          description: Only entities of this type.
          schema:
            type: string
            maxLength: 32
        - name: q
          in: query
          required: false
          description: Case-insensitive substring match on the entity name.
          schema:
            type: string
            maxLength: 200
        - name: document_id
          in: query
          required: false
          description: Only entities extracted from this document.
          schema:
            type: string
            pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
      responses:
        "200":
          description: Page of live entities.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GraphEntityList"
              example:
                total: 4211
                limit: 100
                offset: 0
                entities:
                  - entity_id: "7d6c5b4a-3e2f-4d1c-8b0a-9f8e7d6c5b4a"
                    name: "Acme Water Board"
                    entity_type: organization
                    aliases: ["AWB"]
                    properties: {}
                    source_doc_id: "3f7c2a90-51b4-4e2a-9c0d-8a1b2c3d4e5f"
                    source_system: document-library
                    confidence: 0.93
                    canonical_id: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/graph/triples:
    get:
      operationId: listGraphTriples
      tags: [Knowledge Graph]
      summary: List knowledge-graph relations
      x-apex-availability: available
      description: |
        Pages through live subject–predicate–object relations, highest extraction confidence
        first. Pass `entity` to see only relations where that exact name appears as the subject
        or the object.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: limit
          in: query
          required: false
          description: Maximum number of relations to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 200
        - name: offset
          in: query
          required: false
          description: Number of relations to skip.
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: entity
          in: query
          required: false
          description: Exact entity name to match as subject or object.
          schema:
            type: string
            maxLength: 512
      responses:
        "200":
          description: Page of live relations.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GraphTripleList"
              example:
                total: 9877
                limit: 200
                offset: 0
                triples:
                  - triple_id: "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d"
                    subject: "Acme Water Board"
                    predicate: governed_by
                    object: "National Water Act"
                    properties: {}
                    source_doc_id: "3f7c2a90-51b4-4e2a-9c0d-8a1b2c3d4e5f"
                    source_system: document-library
                    confidence: 0.88
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/graph/communities:
    get:
      operationId: listGraphCommunities
      tags: [Knowledge Graph]
      summary: List knowledge-graph communities
      x-apex-availability: available
      description: |
        Returns detected entity communities, largest first, each with its member entities and a
        generated summary. Use `min_size` to hide small fragments.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: min_size
          in: query
          required: false
          description: Only communities with at least this many member entities.
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          required: false
          description: Maximum number of communities to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 100
      responses:
        "200":
          description: Detected communities, largest first.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GraphCommunityList"
              example:
                total: 1
                communities:
                  - community_id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
                    name: "Water infrastructure compliance"
                    level: 0
                    member_entities: ["Acme Water Board", "National Water Act"]
                    entity_count: 2
                    summary: "Entities governing water infrastructure approvals and compliance."
                    summary_model: "claude-haiku"
                    summary_cost_zar: 0.04
                    detection_run_id: "7c9d2e4f6a1b"
                    resolution_param: 1.0
                    created_at: "2026-08-17T22:10:05+00:00"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/graph/atlas:
    get:
      operationId: getGraphAtlas
      tags: [Knowledge Graph]
      summary: Knowledge-Graph Atlas overview
      x-apex-availability: available
      description: |
        The Atlas overview — a map of the whole knowledge graph at community resolution. Returns
        aggregate counts (`meta`), every community with its size and top entity types, and
        undirected inter-community edges weighted by how many relations cross between them. Use
        it to render a graph overview, then drill in with the community and entity endpoints.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      responses:
        "200":
          description: Atlas overview — counts, communities, and inter-community edges.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AtlasOverview"
              example:
                meta:
                  tenant_id: acme
                  generated_at: "2026-08-18T09:40:12+00:00"
                  entities: 4180
                  triples: 9877
                  communities: 37
                  documents: 1042
                  unclustered_entities: 512
                communities:
                  - community_id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
                    name: "Water infrastructure compliance"
                    summary: "Entities governing water infrastructure approvals and compliance."
                    entity_count: 214
                    top_entity_types: {organization: 88, standard: 61, project: 32}
                edges:
                  - source: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
                    target: "8c9d0e1f-2a3b-4c5d-8e6f-7a8b9c0d1e2f"
                    weight: 63
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/graph/atlas/community/{community_id}:
    get:
      operationId: getAtlasCommunity
      tags: [Knowledge Graph]
      summary: Expand one Atlas community
      x-apex-availability: available
      description: |
        Expands one community into its member entities and internal relations. Nodes aggregate
        every live mention of an entity: modal type, best confidence, mention count, and the
        number of distinct source documents. Edges are relations whose both endpoints belong to
        the community. Responses are capped at 1,000 nodes and 500 edges, strongest first.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: community_id
          in: path
          required: true
          description: Community identifier from the Atlas overview.
          schema:
            type: string
            pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
      responses:
        "200":
          description: Community detail with member nodes and intra-community edges.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AtlasCommunity"
              example:
                community:
                  community_id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
                  name: "Water infrastructure compliance"
                  summary: "Entities governing water infrastructure approvals and compliance."
                  entity_count: 214
                nodes:
                  - id: "Acme Water Board"
                    name: "Acme Water Board"
                    entity_type: organization
                    confidence: 0.93
                    mentions: 41
                    doc_count: 18
                edges:
                  - subject: "Acme Water Board"
                    predicate: governed_by
                    object: "National Water Act"
                    confidence: 0.88
                    count: 6
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Unknown community identifier, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Community '2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e' not found"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/graph/atlas/entity:
    get:
      operationId: getAtlasEntity
      tags: [Knowledge Graph]
      summary: Entity dossier
      x-apex-availability: available
      description: |
        A dossier for one entity, looked up by exact name (known aliases resolve to their
        canonical entity). Returns the aggregated profile across all live mentions, the
        communities it belongs to, its inbound and outbound relations, and up to 200 evidence
        documents — each with up to two short passage excerpts that mention the entity.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: name
          in: query
          required: true
          description: Exact entity name (or a known variant of it).
          schema:
            type: string
            maxLength: 512
      responses:
        "200":
          description: Entity profile, communities, relations, and evidence documents.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AtlasEntity"
              example:
                entity:
                  name: "Acme Water Board"
                  entity_type: organization
                  aliases: ["AWB"]
                  confidence: 0.93
                  mentions: 41
                  properties: {}
                communities:
                  - community_id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
                    name: "Water infrastructure compliance"
                relations:
                  outbound:
                    - {predicate: governed_by, object: "National Water Act", confidence: 0.88, count: 6}
                  inbound:
                    - {predicate: contracts_with, subject: "Alpha Constructors", confidence: 0.79, count: 3}
                evidence:
                  - document_id: "3f7c2a90-51b4-4e2a-9c0d-8a1b2c3d4e5f"
                    file_name: "handover-pack.pdf"
                    source_id: document-library
                    confidence: 0.93
                    snippets: ["... the Acme Water Board approved the commissioning plan ..."]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No live entity with that name, or the tenant is not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                detail: "Entity 'Acme Water Board' not found"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /api/v1/{tenant_id}/config:
    get:
      operationId: getTenantConfiguration
      tags: [Configuration]
      summary: Read your tenant configuration
      x-apex-availability: available
      description: |
        Read-only view of your tenant's knowledge-engine configuration: display name, storage
        schema name, whether answer verification is enabled, and which embedding and generation
        models are active. Configuration changes are made through your platform operator, not
        through this service.

        Service behaviour: standard rate limits and the 30-second request budget apply.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      responses:
        "200":
          description: Tenant configuration view.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantConfigView"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TenantNotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /health:
    get:
      operationId: getServiceHealth
      tags: [Service]
      summary: Service liveness
      x-apex-availability: available
      security: []
      description: |
        Unauthenticated liveness check. Returns the overall service `status` and the service
        `version`. The payload may include additional diagnostic fields; those fields are not
        part of the stable contract and must not be relied upon. This endpoint is exempt from
        rate limiting and the request timeout.
      responses:
        "200":
          description: Service is reachable. Inspect `status` for overall health.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"
              example:
                status: healthy
                version: "0.1.0"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Single bearer scheme accepting either credential type, presented identically as
        `Authorization: Bearer <credential>`:

        - **Machine-to-machine access token** — a signed JSON Web Token issued to your
          integration client by the APEX identity provider; it carries your tenant scope.
        - **Forge-issued tenant API key** — a static key provisioned by your platform operator,
          scoped to exactly one tenant.

        The credential is validated first (`401` on failure), then the `tenant_id` path segment
        is checked against the credential's tenant scope (`403` on mismatch). Credentials are
        obtained through your Reisiger platform contact.

  parameters:
    TenantId:
      name: tenant_id
      in: path
      required: true
      description: >-
        Your tenant identifier — the first path segment on every tenant-scoped route. Must match
        the tenant your credential is scoped to; a mismatch returns `403`.
      schema:
        type: string
        pattern: "^[a-z0-9][a-z0-9_-]{0,63}$"
      example: your-tenant
    RunId:
      name: run_id
      in: path
      required: true
      description: >-
        Ingestion-run identifier, as returned when the run was created and by the run listing.
        Run identifiers are persistent — they remain valid across service restarts.
      schema:
        type: string
      example: "0f3a1b2c4d5e"
    SourceId:
      name: source_id
      in: path
      required: true
      description: Identifier of a source configured for your tenant.
      schema:
        type: string
      example: document-library
    RawHash:
      name: raw_hash
      in: path
      required: true
      description: >-
        Content hash identifying the failed document, as returned by the failed-document
        listing (`raw_hash` field).
      schema:
        type: string
        pattern: "^[0-9a-f]{64}$"
      example: "9f2b1c0d8e7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c"

  examples:
    ingestionRunQueued:
      summary: A freshly accepted ingestion run
      value:
        run_id: "0f3a1b2c4d5e"
        tenant_id: acme
        kind: ingest
        status: queued
        source_ids: [document-library]
        shard_spec:
          - ["Projects/Alpha"]
          - ["Projects/Beta", "Projects/Gamma"]
        params:
          force_reingest: true
          incremental: null
          max_concurrent_lanes: 2
        current_stage: ""
        lanes_total: 2
        lane_state: {}
        counters: {}
        report: null
        error: null
        started_by: api
        claimed_by: null
        cancel_requested: false
        created_at: "2026-08-18T08:55:27Z"
        started_at: null
        finished_at: null
        heartbeat_at: null
        claimed_at: null
    knowledgeGraphRunQueued:
      summary: A freshly queued knowledge-graph extraction job
      value:
        run_id: "3c1d9e7f5a2b"
        tenant_id: acme
        kind: kg_trailing
        status: queued
        source_ids: []
        shard_spec: null
        params:
          force_reingest: false
          incremental: null
          max_concurrent_lanes: 1
        current_stage: ""
        lanes_total: 1
        lane_state: {}
        counters: {}
        report: null
        error: null
        started_by: api
        claimed_by: null
        cancel_requested: false
        created_at: "2026-08-18T08:58:03Z"
        started_at: null
        finished_at: null
        heartbeat_at: null
        claimed_at: null

  headers:
    XRateLimitLimit:
      description: >-
        Requests-per-minute quota for the throttling scope that produced this response (tenant
        scope on success; the rejecting scope on `429`).
      schema:
        type: integer
    XRateLimitRemaining:
      description: Requests remaining in the current one-minute window.
      schema:
        type: integer
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
    XRateLimitScope:
      description: >-
        Present with value `credential` when the per-credential layer (rather than the per-tenant
        layer) produced the rejection.
      schema:
        type: string

  responses:
    Unauthorized:
      description: Missing or invalid credential.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            missingHeader:
              value:
                detail: "Missing Authorization header"
            invalidCredential:
              value:
                detail: "Invalid credentials"
    Forbidden:
      description: >-
        The credential is valid but scoped to a different tenant than the one in the URL path.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            detail: "Credential scoped to tenant 'your-tenant', cannot access 'other-tenant'"
    TenantNotFound:
      description: The tenant in the URL path is not registered with the knowledge engine.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            detail: "Tenant 'your-tenant' not found"
    ValidationError:
      description: >-
        Request validation failed — a body field, query parameter, or path parameter did not
        match the declared constraints.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ValidationErrorResponse"
    RateLimited:
      description: >-
        Rate limit exceeded. Two independent limits apply: per tenant (default 120 requests per
        minute, shared across all of the tenant's callers) and per credential (default 60
        requests per minute). Rejections occur before authentication. Honour `Retry-After`
        (60 seconds) before retrying; `X-RateLimit-Scope: credential` marks a per-credential
        rejection.
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Scope:
          $ref: "#/components/headers/XRateLimitScope"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/RateLimitError"
          examples:
            tenantLimit:
              value:
                detail: "Tenant rate limit exceeded. Try again later."
                retry_after_seconds: 60
            credentialLimit:
              value:
                detail: "Per-credential rate limit exceeded. Try again later."
                retry_after_seconds: 60
    GatewayTimeout:
      description: >-
        The request exceeded its processing budget — 30 seconds for query, search, and read
        operations; 300 seconds for ingestion submission.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/TimeoutError"
          example:
            detail: "Request timed out after 30s"
            timeout_seconds: 30

  schemas:
    # ── Errors ──────────────────────────────────────────────────────────────
    Error:
      type: object
      description: Standard error body.
      required: [detail]
      properties:
        detail:
          type: string
          description: Human-readable explanation of the failure.

    ValidationErrorResponse:
      type: object
      description: Validation failure body. `detail` lists each violated constraint.
      x-apex-note: "Schema partially documented — verify response against the service."
      properties:
        detail:
          type: array
          items:
            type: object
            properties:
              loc:
                type: array
                description: Path to the offending field (location, then field names/indexes).
                items:
                  oneOf:
                    - type: string
                    - type: integer
              msg:
                type: string
                description: Human-readable message.
              type:
                type: string
                description: Machine-readable violation code.

    RateLimitError:
      type: object
      description: Rate-limit rejection body.
      required: [detail]
      properties:
        detail:
          type: string
        retry_after_seconds:
          type: integer
          description: Seconds to wait before retrying (mirrors the `Retry-After` header).

    TimeoutError:
      type: object
      description: Timeout rejection body.
      required: [detail]
      properties:
        detail:
          type: string
        timeout_seconds:
          type: integer
          description: The processing budget that was exceeded, in seconds.

    # ── Query ───────────────────────────────────────────────────────────────
    QueryRequest:
      type: object
      description: A single query against the tenant knowledge base.
      required: [query]
      properties:
        query:
          type: string
          minLength: 1
          maxLength: 2000
          description: The question or instruction to run against the knowledge base.
        mode:
          type: string
          enum: [rag, extract, match]
          default: rag
          description: >-
            Query mode. `rag` (default) returns a grounded prose answer with citations.
            `extract` returns structured JSON, optionally shaped by `output_schema`.
            `match` scores the corpus against each entry in `match_criteria`.
        match_criteria:
          type: ["array", "null"]
          items:
            type: string
          description: >-
            Required when `mode` is `match` — the list of requirements to score the corpus
            against.
        output_schema:
          type: ["object", "null"]
          description: >-
            Optional JSON Schema hint shaping the structured output when `mode` is `extract`.
        user_context:
          type: ["object", "null"]
          description: >-
            Optional caller context passed through to the pipeline. The service sets the
            `subscription_tier` key from your credential's entitlement tier; a caller-supplied
            value for that key is overwritten and cannot escalate entitlement.

    SourceCitation:
      type: object
      description: A source passage cited by a generated answer.
      required: [chunk_id, document_id, source_entity, relevance_score]
      properties:
        chunk_id:
          type: string
          description: Identifier of the cited passage.
        document_id:
          type: string
          description: Identifier of the document the passage belongs to.
        source_entity:
          type: string
          description: The source system or collection the document came from.
        relevance_score:
          type: number
          description: Retrieval relevance of this passage to the query.

    UsageMetrics:
      type: object
      description: Token usage and cost for the language-model call behind an answer.
      properties:
        input_tokens:
          type: integer
          default: 0
        output_tokens:
          type: integer
          default: 0
        total_tokens:
          type: integer
          default: 0
        cost_zar:
          type: number
          default: 0.0
          description: Cost of the call in South African Rand.
        provider:
          type: string
          default: ""
          description: Language-model provider that served the call.
        model:
          type: string
          default: ""
          description: Model identifier that served the call.

    ClaimVerification:
      type: object
      description: Verification result for a single factual claim in the answer.
      required: [claim, confidence]
      properties:
        claim:
          type: string
          description: The claim extracted from the answer.
        confidence:
          type: number
          description: Verification confidence for this claim.
        evidence_chunk_id:
          type: ["string", "null"]
          description: Passage identifier of the supporting evidence, when found.
        label:
          type: string
          default: ""
          description: >-
            Verification label — `ENTAILMENT` (supported), `CONTRADICTION` (contradicted), or
            `NEUTRAL` (no decisive evidence).

    VerificationResponse:
      type: object
      description: >-
        Claim-verification result attached to an answer when verification is enabled for the
        tenant. Each factual claim in the answer is checked against the retrieved evidence.
      properties:
        enabled:
          type: boolean
          default: true
          description: Whether verification ran for this answer.
        claims_checked:
          type: integer
          default: 0
          description: Number of claims extracted and checked.
        confidence:
          type: number
          default: 0.0
          description: Aggregate verification confidence across all checked claims.
        status:
          type: string
          enum: [HIGH, MEDIUM, LOW, SKIPPED]
          default: SKIPPED
          description: >-
            Overall verification banding. `SKIPPED` when verification is disabled or did not run.
        claim_details:
          type: array
          items:
            $ref: "#/components/schemas/ClaimVerification"
          description: Per-claim verification results.
        external_verdict:
          type: ["object", "null"]
          description: >-
            Verdict from the external verification service, populated only when an escalation ran
            synchronously. Absent or null otherwise.
          x-apex-note: "Schema partially documented — verify response against the service."
        external_trigger:
          type: ["string", "null"]
          description: Name of the trigger that caused external escalation, when one fired.

    QueryResponse:
      type: object
      description: The generated answer and everything needed to trust it.
      required: [query, answer]
      properties:
        query:
          type: string
          description: The query as processed.
        answer:
          type: string
          description: >-
            The generated output. Prose for `rag` mode; a JSON document (returned as a string)
            for `extract` and `match` modes.
        sources:
          type: array
          items:
            $ref: "#/components/schemas/SourceCitation"
          description: Passages the answer is grounded in.
        verification:
          anyOf:
            - $ref: "#/components/schemas/VerificationResponse"
            - type: "null"
          description: Claim-verification result; null when verification is disabled.
        model_used:
          type: string
          default: ""
          description: Identifier of the model that generated the answer.
        elapsed_ms:
          type: number
          default: 0.0
          description: End-to-end processing time in milliseconds.
        usage:
          $ref: "#/components/schemas/UsageMetrics"
        pipeline_used:
          type: string
          default: ""
          description: >-
            Which answering pipeline produced this response — `rag` or `context`. In batch
            responses, failed items carry the value `error`.
        response_confidence:
          type: number
          default: 0.0
          description: Aggregate confidence in the answer, between 0 and 1.

    CostBreakdown:
      type: object
      description: Per-request cost breakdown in South African Rand.
      properties:
        total_zar:
          type: number
          default: 0.0
          description: Total cost of the request in South African Rand.
        embedding_zar:
          type: number
          default: 0.0
          description: Portion spent embedding the query.
        generation_zar:
          type: number
          default: 0.0
          description: Portion spent generating the answer.
        verification_zar:
          type: number
          default: 0.0
          description: Portion spent verifying the answer.
        model_used:
          type: string
          default: ""
          description: Model the cost is attributed to.
        input_tokens:
          type: integer
          default: 0
        output_tokens:
          type: integer
          default: 0

    QueryResponseEnvelope:
      type: object
      description: Standard answer envelope — the answer plus its cost.
      required: [data, cost]
      properties:
        data:
          $ref: "#/components/schemas/QueryResponse"
        cost:
          $ref: "#/components/schemas/CostBreakdown"

    BatchQueryRequest:
      type: object
      description: A batch of 1–50 queries to run in parallel.
      required: [queries]
      properties:
        queries:
          type: array
          minItems: 1
          maxItems: 50
          items:
            $ref: "#/components/schemas/QueryRequest"

    BatchQueryResponseEnvelope:
      type: object
      description: Aggregated results for a batch of queries, in submission order.
      required: [results, total_cost, query_count]
      properties:
        results:
          type: array
          items:
            $ref: "#/components/schemas/QueryResponseEnvelope"
          description: >-
            One envelope per submitted query, in order. Failed items are envelopes with
            `data.pipeline_used` set to `error` and zero cost.
        total_cost:
          $ref: "#/components/schemas/CostBreakdown"
        query_count:
          type: integer
          description: Number of result envelopes returned.

    # ── Search ──────────────────────────────────────────────────────────────
    RetrievalResult:
      type: object
      description: A single ranked passage from retrieval-only search.
      required:
        [chunk_id, document_id, content, score, source_id, source_entity, chunk_index]
      properties:
        chunk_id:
          type: string
          description: Identifier of the passage.
        document_id:
          type: string
          description: Identifier of the parent document.
        content:
          type: string
          description: The passage text.
        score:
          type: number
          description: Relevance score for the query.
        source_id:
          type: string
          description: Identifier of the configured source the document came from.
        source_entity:
          type: string
          description: The source system or collection the document came from.
        chunk_index:
          type: integer
          description: Position of this passage within its document.
        access_level:
          type: string
          default: tenant
          description: Access classification of the passage.
        metadata:
          type: object
          description: Free-form passage metadata captured at ingestion time.

    SearchResponse:
      type: object
      description: Retrieval-only search results.
      required: [query, results, total, total_available, elapsed_ms]
      properties:
        query:
          type: string
          description: The search text as processed.
        results:
          type: array
          items:
            $ref: "#/components/schemas/RetrievalResult"
          description: Ranked passages, best first, trimmed to `limit`.
        total:
          type: integer
          description: Number of results returned in this response.
        total_available:
          type: integer
          description: >-
            Number of results the retrieval engine produced before your `limit` was applied.
        elapsed_ms:
          type: number
          description: Retrieval time in milliseconds.

    # ── Ingestion ───────────────────────────────────────────────────────────
    IngestRequest:
      type: object
      description: >-
        Ingestion run options. Note — both fields are accepted but not yet honoured: the current
        release always runs the tenant's full configured source set at normal priority.
      properties:
        source:
          type: ["string", "null"]
          description: >-
            Reserved for source-scoped runs. Accepted but ignored in the current release.
        priority:
          type: string
          default: normal
          description: >-
            Reserved for prioritised runs. Accepted but ignored in the current release.

    StageResult:
      type: object
      description: Metrics from one stage of the ingestion pipeline.
      required: [stage_name]
      properties:
        stage_name:
          type: string
          description: Name of the pipeline stage.
        records_in:
          type: integer
          default: 0
          description: Records entering the stage.
        records_out:
          type: integer
          default: 0
          description: Records the stage emitted.
        records_skipped:
          type: integer
          default: 0
          description: Records the stage deliberately skipped.
        records_failed:
          type: integer
          default: 0
          description: Records that failed in the stage.
        duration_ms:
          type: number
          default: 0.0
          description: Stage duration in milliseconds.
        metadata:
          type: object
          description: Free-form stage diagnostics.

    DocumentDisposition:
      type: object
      description: >-
        The terminal outcome for one input document — every document that enters a run receives
        exactly one disposition.
      required: [record_id, status]
      properties:
        record_id:
          type: string
          description: Stable identifier of the input record.
        document_id:
          type: ["string", "null"]
          description: Parsed-document identifier, set once parsing succeeded.
        file_name:
          type: ["string", "null"]
          description: Original file name, when known.
        source_id:
          type: string
          default: ""
          description: Identifier of the configured source the record came from.
        status:
          type: string
          enum: [indexed, duplicate, parked]
          description: >-
            `indexed` — embedded and stored; `duplicate` — dropped as a duplicate; `parked` —
            failed a stage or produced no usable output, held with a reason.
        reason:
          type: string
          default: ""
          description: Why the record was parked or which record it duplicates; empty when indexed.
        stage:
          type: string
          default: ""
          description: The pipeline stage that assigned this disposition.

    IngestionReport:
      type: object
      description: >-
        Reconciliation of an ingestion run — every input record is accounted for as indexed,
        duplicate, or parked. A run with unaccounted records is reported as failed rather than
        silently succeeding.
      properties:
        run_id:
          type: string
          default: ""
          description: Identifier of the ingestion run.
        records_in:
          type: integer
          default: 0
          description: Records that entered the pipeline.
        indexed:
          type: integer
          default: 0
          description: Records embedded and stored in the index.
        duplicates:
          type: integer
          default: 0
          description: Records dropped as duplicates.
        parked:
          type: integer
          default: 0
          description: Records held with a reason after failing a stage or yielding no output.
        unaccounted:
          type: integer
          default: 0
          description: >-
            Records with no terminal disposition. Any value above zero marks the run incomplete
            and the job failed.
        resume_skipped:
          type: integer
          default: 0
          description: >-
            Records skipped at intake because identical content was already processed by an
            earlier run. Also counted within `duplicates`; broken out for visibility.
        complete:
          type: boolean
          default: false
          description: >-
            True only when every input record has a disposition and the counts balance.
        total_cost_zar:
          type: number
          default: 0.0
          description: Aggregate model spend for the run in South African Rand.
        dispositions:
          type: array
          items:
            $ref: "#/components/schemas/DocumentDisposition"
          description: Per-document outcomes.
        stats:
          type: object
          description: >-
            Per-run statistics gathered while the run executed: a file-type breakdown,
            page-image text-recognition document and page counts, per-stage timings, and a
            failure-reason table. Keys are informational and may evolve; treat this object as
            free-form diagnostics.

    IngestJob:
      type: object
      description: An asynchronous ingestion run and its current state.
      required: [job_id, tenant_id, status, created_at]
      properties:
        job_id:
          type: string
          description: Identifier to poll the job with.
        tenant_id:
          type: string
          description: Tenant the run belongs to.
        status:
          type: string
          enum: [queued, running, completed, failed]
          description: Job lifecycle state.
        created_at:
          type: string
          format: date-time
          description: Submission time (UTC).
        current_stage:
          type: string
          default: ""
          description: Live progress string for the stage currently running.
        results:
          type: ["array", "null"]
          items:
            $ref: "#/components/schemas/StageResult"
          description: Per-stage metrics, populated when the run finishes.
        ingestion_report:
          anyOf:
            - $ref: "#/components/schemas/IngestionReport"
            - type: "null"
          description: Full reconciliation report, populated when the run finishes.
        elapsed_ms:
          type: ["number", "null"]
          description: Total run time in milliseconds, populated when the run finishes.
        error:
          type: ["string", "null"]
          description: Failure description when `status` is `failed`.
        cost:
          anyOf:
            - $ref: "#/components/schemas/CostBreakdown"
            - type: "null"
          description: >-
            Reserved. The authoritative run cost is `ingestion_report.total_cost_zar`.

    # ── Configuration ───────────────────────────────────────────────────────
    TenantConfigView:
      type: object
      description: Read-only view of a tenant's knowledge-engine configuration.
      required:
        - tenant_id
        - display_name
        - schema_name
        - verification_enabled
        - embedding_model
        - embedding_provider
        - generation_model
        - generation_provider
      properties:
        tenant_id:
          type: string
          description: Tenant identifier.
        display_name:
          type: string
          description: Human-readable tenant name.
        schema_name:
          type: string
          description: Name of the isolated storage schema holding this tenant's index.
        verification_enabled:
          type: boolean
          description: Whether answers are claim-verified for this tenant.
        embedding_model:
          type: string
          description: Embedding model used to index and query the corpus.
        embedding_provider:
          type: string
          description: Provider of the embedding model.
        generation_model:
          type: string
          description: Model used to generate answers.
        generation_provider:
          type: string
          description: Provider of the generation model.

    # ── Service ─────────────────────────────────────────────────────────────
    HealthStatus:
      type: object
      description: >-
        Liveness payload. Only `status` and `version` are part of the stable contract;
        additional diagnostic fields may appear and must not be relied upon.
      required: [status, version]
      properties:
        status:
          type: string
          enum: [healthy, degraded, unhealthy]
          description: Overall service health.
        version:
          type: string
          description: Service version.

    # ── Ingestion runs ──────────────────────────────────────────────────────
    StartIngestionRunRequest:
      type: object
      description: >-
        Options for a durable ingestion run. All fields are optional — an empty body starts a
        full run over every configured source.
      properties:
        source_ids:
          type: ["array", "null"]
          items:
            type: string
          description: >-
            Restrict the run to these configured sources. Omit or `null` to run every source.
        shard_spec:
          type: ["array", "null"]
          items:
            type: array
            items:
              type: string
          description: >-
            Explicit lane definition: one lane per inner array, each inner array listing the
            path prefixes that lane crawls. A run with a `shard_spec` re-crawls just those
            prefixes and never advances the source-wide delta checkpoint.
        force_reingest:
          type: boolean
          default: false
          description: Re-process documents even when identical content was already ingested.
        incremental:
          type: ["boolean", "null"]
          description: >-
            Force delta-only crawling (`true`) or full recursive crawling (`false`). Omit or
            `null` to let the run decide: full-source runs use the source's delta checkpoint
            when one exists; prefix-scoped runs always crawl recursively.
        max_concurrent_lanes:
          type: integer
          minimum: 1
          maximum: 4
          default: 1
          description: >-
            Lanes processed concurrently within the run. Above 1 without a `shard_spec`,
            lanes are generated automatically by stable-hash partitioning of file paths.

    IngestionRunParams:
      type: object
      description: The caller-supplied options the run was created with.
      properties:
        force_reingest:
          type: boolean
          default: false
          description: Whether already-ingested content is re-processed.
        incremental:
          type: ["boolean", "null"]
          description: Forced crawl mode; `null` when the run decides per source.
        max_concurrent_lanes:
          type: integer
          minimum: 1
          maximum: 4
          default: 1
          description: Lanes processed concurrently within the run.

    IngestionRunLane:
      type: object
      description: >-
        Per-lane state. A lane is either prefix-scoped (`prefixes` non-empty) or hash-scoped
        (`shard_index`/`shard_count` set — files partitioned by a stable hash of their path).
      required: [lane]
      properties:
        lane:
          type: integer
          description: Zero-based lane number.
        prefixes:
          type: array
          items:
            type: string
          description: Path prefixes this lane crawls; empty for hash-scoped lanes.
        shard_index:
          type: ["integer", "null"]
          description: This lane's partition index, for hash-scoped lanes.
        shard_count:
          type: ["integer", "null"]
          description: Total number of hash partitions, for hash-scoped lanes.
        status:
          type: string
          enum: [queued, running, completed, failed, cancelled, interrupted]
          description: Lane lifecycle state.
        current_stage:
          type: string
          default: ""
          description: Live progress string for the lane's current stage.
        counters:
          type: object
          description: Free-form per-lane pipeline counters.
        error:
          type: ["string", "null"]
          description: Failure description when the lane failed.

    IngestionRun:
      type: object
      description: >-
        A persistent ingestion or knowledge-graph run. Run records survive service restarts;
        a run that was executing during a restart is later reported as `interrupted` and can
        be resumed.
      required: [run_id, tenant_id, kind, status]
      properties:
        run_id:
          type: string
          description: Persistent run identifier.
        tenant_id:
          type: string
          description: Tenant the run belongs to.
        kind:
          type: string
          enum: [ingest, kg_trailing, kg_refine]
          description: >-
            `ingest` — a document ingestion run; `kg_trailing` — the deferred knowledge-graph
            extraction pass; `kg_refine` — the knowledge-graph refinement pass.
        status:
          type: string
          enum: [queued, running, completed, failed, cancelled, interrupted]
          description: >-
            Run lifecycle state. `interrupted` marks a run whose execution was lost to a
            service restart; `interrupted`, `failed`, and `cancelled` runs are resumable.
        source_ids:
          type: array
          items:
            type: string
          description: Sources the run covers; empty means every configured source.
        shard_spec:
          type: ["array", "null"]
          items:
            type: array
            items:
              type: string
          description: Explicit lane definition the run was created with, when provided.
        params:
          $ref: "#/components/schemas/IngestionRunParams"
        current_stage:
          type: string
          default: ""
          description: Live progress string for the stage currently running.
        lanes_total:
          type: integer
          default: 1
          description: Number of lanes in the run.
        lane_state:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/IngestionRunLane"
          description: Per-lane state, keyed by lane number.
        counters:
          type: object
          description: Free-form aggregate pipeline counters.
        report:
          anyOf:
            - $ref: "#/components/schemas/IngestionReport"
            - type: "null"
          description: Full reconciliation report, populated when the run finishes.
        error:
          type: ["string", "null"]
          description: Failure description when `status` is `failed`.
        started_by:
          type: string
          default: api
          description: What initiated the run — for example `api` or a schedule.
        claimed_by:
          type: ["string", "null"]
          description: Identifier of the service instance executing the run, once claimed.
        cancel_requested:
          type: boolean
          default: false
          description: >-
            True once cancellation has been requested; the run stops at its next progress
            checkpoint.
        created_at:
          type: ["string", "null"]
          description: Submission time (UTC).
        started_at:
          type: ["string", "null"]
          description: Execution start time, once the run leaves `queued`.
        finished_at:
          type: ["string", "null"]
          description: Completion time, once the run reaches a terminal status.
        heartbeat_at:
          type: ["string", "null"]
          description: Last liveness signal from the executing service instance.
        claimed_at:
          type: ["string", "null"]
          description: When the executing service instance claimed the run.

    IngestionRunList:
      type: object
      description: One page of runs, newest first.
      required: [runs]
      properties:
        runs:
          type: array
          items:
            $ref: "#/components/schemas/IngestionRun"

    IngestionRunProgress:
      type: object
      description: >-
        Live progress snapshot for a run: per-stage document funnel, windowed throughput
        rates, a page-image text-recognition gauge, and a completion estimate.
      required: [run_id, status, lanes_total, lanes_done, owned]
      properties:
        run_id:
          type: string
          description: Run identifier.
        status:
          type: string
          enum: [queued, running, completed, failed, cancelled, interrupted]
          description: Run lifecycle state.
        current_stage:
          type: string
          description: Live progress string for the stage currently running.
        lanes_total:
          type: integer
          description: Number of lanes in the run.
        lanes_done:
          type: integer
          description: Lanes that have completed.
        lanes:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/IngestionRunLane"
          description: Per-lane state, keyed by lane number.
        stages:
          type: object
          description: >-
            Per-stage counters (documents in/out per pipeline stage), keyed by stage name.
        rates:
          type: ["object", "null"]
          description: >-
            Windowed throughput rates per stage. May be absent when the responding service
            instance is not executing the run.
        ocr:
          type: ["object", "null"]
          description: >-
            Page-image text-recognition gauge: queued and in-flight documents and completed
            page counts. May be absent when no recognition work is active.
        expected_total:
          type: ["integer", "null"]
          description: Estimated total number of documents the run will process.
        processed:
          type: integer
          description: Documents processed so far.
        eta_seconds:
          type: ["number", "null"]
          description: Estimated seconds to completion, when derivable from current rates.
        owned:
          type: boolean
          description: >-
            Whether the responding service instance is executing the run. When `false`, rates
            and the recognition gauge may be stale or absent and counters are historical.
        claimed_by:
          type: ["string", "null"]
          description: Identifier of the service instance executing the run, once claimed.
        heartbeat_at:
          type: ["string", "null"]
          description: Last liveness signal from the executing service instance.

    # ── Continuous sync ─────────────────────────────────────────────────────
    SyncStateList:
      type: object
      description: Incremental-sync checkpoints, one row per source that has synced.
      required: [sources]
      properties:
        sources:
          type: array
          items:
            type: object
            required: [source_id, has_delta_token]
            properties:
              source_id:
                type: string
                description: Configured source identifier.
              has_delta_token:
                type: boolean
                description: >-
                  Whether a delta checkpoint is held. With one, the next incremental run
                  re-crawls only what changed; without one, it performs a full crawl.
              last_run_id:
                type: ["string", "null"]
                description: The run that last advanced this checkpoint.
              last_synced_at:
                type: ["string", "null"]
                description: When the source last completed a sync (UTC).
              last_discovered:
                type: ["integer", "null"]
                description: Items discovered by the last sync pass.
              last_fetch_errors:
                type: ["integer", "null"]
                description: Items the last sync pass failed to fetch.
              updated_at:
                type: ["string", "null"]
                description: When this checkpoint row last changed (UTC).

    SyncSchedule:
      type: object
      description: A source's continuous-sync schedule.
      required: [source_id, enabled, paused, interval_minutes]
      properties:
        source_id:
          type: string
          description: Configured source identifier.
        enabled:
          type: boolean
          description: Whether the schedule is active at all.
        paused:
          type: boolean
          description: Whether enqueueing is temporarily suspended.
        interval_minutes:
          type: integer
          minimum: 5
          maximum: 10080
          description: Cadence between enqueued sync runs, in minutes.
        scope_prefixes:
          type: ["array", "null"]
          items:
            type: string
          description: >-
            When set, scheduled runs re-crawl just these path prefixes and never advance the
            source-wide delta checkpoint. `null` for full-source schedules.
        last_enqueued_run_id:
          type: ["string", "null"]
          description: The most recent run this schedule enqueued — auditable via the runs endpoints.
        last_enqueued_at:
          type: ["string", "null"]
          description: When the most recent run was enqueued (UTC).
        next_due_at:
          type: ["string", "null"]
          description: When the next run becomes due (UTC).
        updated_by:
          type: string
          description: Who last changed the schedule.
        updated_at:
          type: ["string", "null"]
          description: When the schedule last changed (UTC).

    SyncScheduleList:
      type: object
      description: All sync schedules for the tenant.
      required: [schedules]
      properties:
        schedules:
          type: array
          items:
            $ref: "#/components/schemas/SyncSchedule"

    SyncScheduleRequest:
      type: object
      description: Cadence and scope for a source's sync schedule.
      properties:
        interval_minutes:
          type: integer
          minimum: 5
          maximum: 10080
          default: 60
          description: Cadence between enqueued sync runs — 5 minutes to 7 days.
        scope_prefixes:
          type: ["array", "null"]
          items:
            type: string
          description: >-
            Restrict scheduled runs to these path prefixes. Scoped runs never advance the
            source-wide delta checkpoint. Omit or `null` for full-source syncs.
        enabled:
          type: boolean
          default: true
          description: Whether the schedule is active.

    SyncNowRequest:
      type: object
      description: Options for an immediate sync. An empty body performs a full delta sync.
      properties:
        prefixes:
          type: ["array", "null"]
          items:
            type: string
          description: >-
            Sync just these path prefixes. Scoped runs re-crawl those prefixes and never
            advance the source-wide delta checkpoint. Omit or `null` for a full incremental
            delta sync.

    # ── Failed documents ────────────────────────────────────────────────────
    FailedDocument:
      type: object
      description: One document that failed ingestion, with its remediation state.
      required: [raw_hash, status]
      properties:
        raw_hash:
          type: string
          pattern: "^[0-9a-f]{64}$"
          description: Content hash identifying the failed document across runs.
        file_name:
          type: string
          description: Original file name.
        file_path:
          type: string
          description: Path of the file within its source.
        source_id:
          type: string
          description: Source the document came from.
        status:
          type: string
          description: Ledger status; failed documents carry `parked`.
        reason:
          type: string
          description: Why ingestion failed — for example `no text extracted`.
        stage:
          type: string
          description: The pipeline stage where the failure occurred.
        run_id:
          type: string
          description: The run in which the failure was recorded.
        resolution:
          type: string
          enum: ["", dismissed, replaced]
          description: >-
            Remediation outcome — empty while the failure is open, `dismissed` when accepted
            with a note, `replaced` when a replacement file was ingested.
        resolution_note:
          type: string
          description: Audit note recorded with the resolution.
        resolved_by:
          type: string
          description: Who resolved the record.
        resolved_at:
          type: ["string", "null"]
          description: When the record was resolved (UTC).
        replacement_document_id:
          type: ["string", "null"]
          description: Identifier of the replacement document, for `replaced` records.
        updated_at:
          type: ["string", "null"]
          description: When the record last changed (UTC).

    FailedDocumentQueue:
      type: object
      description: >-
        One page of the failed-document queue plus aggregate breakdowns over the full
        filtered population (not just the returned page).
      required: [items, total, limit, offset, summary]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/FailedDocument"
        total:
          type: integer
          description: Total records matching the filters.
        limit:
          type: integer
          description: Page size applied.
        offset:
          type: integer
          description: Records skipped.
        summary:
          type: object
          description: Aggregates over the full filtered population.
          properties:
            open:
              type: integer
              description: Open failures awaiting remediation.
            dismissed:
              type: integer
              description: Failures accepted with a note.
            replaced:
              type: integer
              description: Failures resolved by ingesting a replacement file.
            by_stage:
              type: object
              additionalProperties:
                type: integer
              description: Open-failure counts keyed by pipeline stage.
            by_reason:
              type: object
              additionalProperties:
                type: integer
              description: Open-failure counts keyed by failure reason.
            by_source:
              type: object
              additionalProperties:
                type: integer
              description: Open-failure counts keyed by source.

    DismissFailedDocumentRequest:
      type: object
      description: Audit note explaining why the failure is being accepted.
      required: [note]
      properties:
        note:
          type: string
          minLength: 3
          description: Why the document is being dismissed; kept for audit.

    FailedDocumentResolution:
      type: object
      description: Outcome of a dismiss or reopen action.
      required: [raw_hash, resolution]
      properties:
        raw_hash:
          type: string
          pattern: "^[0-9a-f]{64}$"
          description: Content hash of the affected record.
        resolution:
          type: string
          enum: ["", dismissed, replaced]
          description: The record's resolution after the action — empty means open again.

    # ── Corpus browse ───────────────────────────────────────────────────────
    CorpusStats:
      type: object
      description: Aggregate statistics for the ingested corpus.
      properties:
        documents:
          type: integer
          description: Total stored documents, including duplicates.
        duplicates:
          type: integer
          description: Documents flagged as duplicates of other stored content.
        total_bytes:
          type: integer
          description: Total size of stored documents in bytes.
        chunks:
          type: integer
          description: Total stored passages.
        tokens:
          type: integer
          description: Total tokens across all passages.
        last_ingested_at:
          type: ["string", "null"]
          description: When the most recent document was ingested (UTC).
        by_file_type:
          type: array
          items:
            type: object
            properties:
              file_type:
                type: string
                description: File type, or `unknown`.
              count:
                type: integer
          description: Document counts per file type.
        by_source:
          type: array
          items:
            type: object
            properties:
              source_id:
                type: string
              count:
                type: integer
              bytes:
                type: integer
          description: Document counts and byte totals per source.

    CorpusVerificationTotals:
      type: object
      description: >-
        Discovery accounting: every discovered document classified as indexed, duplicate,
        open failure, dismissed, or replaced.
      properties:
        discovered:
          type: integer
          description: Documents discovered by crawls.
        indexed:
          type: integer
          description: Documents that reached the index.
        duplicate:
          type: integer
          description: Documents dropped as duplicates.
        parked_open:
          type: integer
          description: Open failures awaiting remediation — the accounting gap.
        dismissed:
          type: integer
          description: Failures accepted with a note.
        replaced:
          type: integer
          description: Failures resolved by ingesting a replacement file.
        accounted:
          type: integer
          description: indexed + duplicate + dismissed + replaced.
        accounted_pct:
          type: number
          description: Accounted as a percentage of discovered.

    CorpusVerification:
      type: object
      description: >-
        Full-corpus ingestion accounting: discovery totals, the stored-to-retrievable
        pipeline chain, and named integrity checks, rolled up into a traffic-light status.
      required: [status, totals, pipeline, checks, by_source, generated_at]
      properties:
        status:
          type: string
          enum: [green, amber, red]
          description: >-
            `green` — everything accounted and consistent; `amber` — open failures awaiting
            remediation; `red` — at least one integrity check failed.
        totals:
          $ref: "#/components/schemas/CorpusVerificationTotals"
        pipeline:
          type: object
          description: The stored-to-retrievable chain, computed from what is actually stored.
          properties:
            documents:
              type: integer
              description: Stored non-duplicate documents.
            duplicate_documents:
              type: integer
              description: Stored documents flagged as duplicates.
            chunked_documents:
              type: integer
              description: Documents with at least one passage.
            retrievable_documents:
              type: integer
              description: Documents whose every passage has an embedding — fully searchable.
            chunks:
              type: integer
              description: Total stored passages.
            embedded_chunks:
              type: integer
              description: Passages with an embedding.
        checks:
          type: array
          items:
            type: object
            required: [id, label, status, count]
            properties:
              id:
                type: string
                description: >-
                  Stable check identifier — one of `open_parked`, `orphaned_ledger`,
                  `chunkless_documents`, `unembedded_chunks`.
              label:
                type: string
                description: Human-readable check description.
              status:
                type: string
                enum: [ok, warn, fail]
              count:
                type: integer
                description: Number of offending records; zero when the check passes.
          description: Named integrity checks that surface drift the counts alone can hide.
        by_source:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/CorpusVerificationTotals"
              - type: object
                properties:
                  source_id:
                    type: string
          description: The same accounting broken down per source.
        generated_at:
          type: string
          format: date-time
          description: When this accounting view was computed (UTC).

    CorpusDocument:
      type: object
      description: One ingested document's metadata.
      required: [document_id]
      properties:
        document_id:
          type: string
          description: Document identifier.
        source_id:
          type: string
          description: Source the document came from.
        source_path:
          type: string
          description: Path of the file within its source.
        file_name:
          type: string
          description: Original file name.
        file_type:
          type: ["string", "null"]
          description: File type — for example `pdf` or `docx`.
        content_type:
          type: ["string", "null"]
          description: Content category recorded at ingestion.
        file_size_bytes:
          type: ["integer", "null"]
          description: File size in bytes.
        is_duplicate:
          type: boolean
          description: Whether the document was flagged as a duplicate of other stored content.
        ingested_at:
          type: ["string", "null"]
          description: When the document was ingested (UTC).

    CorpusDocumentList:
      type: object
      description: One page of ingested documents, newest first.
      required: [total, limit, offset, documents]
      properties:
        total:
          type: integer
          description: Total documents matching the filters.
        limit:
          type: integer
          description: Page size applied.
        offset:
          type: integer
          description: Documents skipped.
        documents:
          type: array
          items:
            allOf:
              - $ref: "#/components/schemas/CorpusDocument"
              - type: object
                properties:
                  chunk_count:
                    type: integer
                    description: Number of passages the document was split into.

    DocumentChunkList:
      type: object
      description: One document's metadata plus its passages in document order.
      required: [document, total, limit, offset, chunks]
      properties:
        document:
          $ref: "#/components/schemas/CorpusDocument"
        total:
          type: integer
          description: Total passages in the document.
        limit:
          type: integer
          description: Page size applied.
        offset:
          type: integer
          description: Passages skipped.
        chunks:
          type: array
          items:
            type: object
            required: [chunk_id, chunk_index, content]
            properties:
              chunk_id:
                type: string
                description: Passage identifier — the unit retrieval and citations operate on.
              chunk_index:
                type: integer
                description: Zero-based position of the passage within the document.
              content:
                type: string
                description: The passage text.
              token_count:
                type: ["integer", "null"]
                description: Token count of the passage.
              chunk_strategy:
                type: ["string", "null"]
                description: The splitting strategy that produced the passage.
              created_at:
                type: ["string", "null"]
                description: When the passage was stored (UTC).

    # ── Knowledge graph ─────────────────────────────────────────────────────
    GraphStats:
      type: object
      description: Health metrics and distributions for the tenant knowledge graph.
      properties:
        live_entities:
          type: integer
          description: Entities currently live in the graph.
        tombstoned_entities:
          type: integer
          description: Entities retired by refinement or supersession.
        live_triples:
          type: integer
          description: Relations currently live in the graph.
        tombstoned_triples:
          type: integer
          description: Relations retired by refinement or supersession.
        source_documents:
          type: integer
          description: Distinct documents that contributed graph rows.
        entity_types:
          type: integer
          description: Distinct entity types in the live graph.
        avg_entity_confidence:
          type: ["number", "null"]
          description: Mean extraction confidence across live entities.
        avg_triple_confidence:
          type: ["number", "null"]
          description: Mean extraction confidence across live relations.
        by_entity_type:
          type: array
          items:
            type: object
            properties:
              entity_type:
                type: string
              count:
                type: integer
          description: Live-entity counts per entity type.
        by_predicate:
          type: array
          items:
            type: object
            properties:
              predicate:
                type: string
              count:
                type: integer
          description: Live-relation counts per predicate.
        communities:
          type: integer
          description: Number of detected communities.

    GraphEntity:
      type: object
      description: One live knowledge-graph entity mention.
      required: [entity_id, name]
      properties:
        entity_id:
          type: string
          description: Entity identifier.
        name:
          type: string
          description: Entity name as extracted.
        entity_type:
          type: ["string", "null"]
          description: Entity type — for example `organization` or `standard`.
        aliases:
          type: ["array", "null"]
          items:
            type: string
          description: Alternative names recorded for the entity.
        properties:
          type: object
          description: Free-form extracted attributes.
        source_doc_id:
          type: ["string", "null"]
          description: The document the mention was extracted from.
        source_system:
          type: ["string", "null"]
          description: The source the document came from.
        confidence:
          type: ["number", "null"]
          description: Extraction confidence, 0 to 1.
        canonical_id:
          type: ["string", "null"]
          description: >-
            When refinement merged this entity into another, the identifier of the canonical
            entity it now resolves to; `null` for canonical entities.

    GraphEntityList:
      type: object
      description: One page of live entities, highest extraction confidence first.
      required: [total, limit, offset, entities]
      properties:
        total:
          type: integer
          description: Total entities matching the filters.
        limit:
          type: integer
          description: Page size applied.
        offset:
          type: integer
          description: Entities skipped.
        entities:
          type: array
          items:
            $ref: "#/components/schemas/GraphEntity"

    GraphTripleList:
      type: object
      description: One page of live relations, highest extraction confidence first.
      required: [total, limit, offset, triples]
      properties:
        total:
          type: integer
          description: Total relations matching the filters.
        limit:
          type: integer
          description: Page size applied.
        offset:
          type: integer
          description: Relations skipped.
        triples:
          type: array
          items:
            type: object
            required: [triple_id, subject, predicate, object]
            properties:
              triple_id:
                type: string
                description: Relation identifier.
              subject:
                type: string
                description: Subject entity name.
              predicate:
                type: string
                description: Relation type.
              object:
                type: string
                description: Object entity name.
              properties:
                type: object
                description: Free-form extracted attributes.
              source_doc_id:
                type: ["string", "null"]
                description: The document the relation was extracted from.
              source_system:
                type: ["string", "null"]
                description: The source the document came from.
              confidence:
                type: ["number", "null"]
                description: Extraction confidence, 0 to 1.

    GraphCommunityList:
      type: object
      description: Detected entity communities, largest first.
      required: [total, communities]
      properties:
        total:
          type: integer
          description: Communities matching the filters.
        communities:
          type: array
          items:
            type: object
            required: [community_id, entity_count]
            properties:
              community_id:
                type: string
                description: Community identifier.
              name:
                type: ["string", "null"]
                description: Generated community name.
              level:
                type: integer
                description: Detection hierarchy level; `0` is the base clustering.
              member_entities:
                type: array
                items:
                  type: string
                description: Names of the community's member entities.
              entity_count:
                type: integer
                description: Number of member entities.
              summary:
                type: ["string", "null"]
                description: Generated summary of what the community covers.
              summary_model:
                type: ["string", "null"]
                description: Model that generated the summary.
              summary_cost_zar:
                type: ["number", "null"]
                description: Cost of generating the summary, in South African Rand.
              detection_run_id:
                type: ["string", "null"]
                description: The run that detected this community.
              resolution_param:
                type: ["number", "null"]
                description: Clustering resolution used during detection.
              created_at:
                type: ["string", "null"]
                description: When the community was detected (UTC).

    AtlasOverview:
      type: object
      description: >-
        The Atlas overview — the whole knowledge graph at community resolution, sized for
        rendering a graph overview.
      required: [meta, communities, edges]
      properties:
        meta:
          type: object
          description: Aggregate counts for the graph.
          properties:
            tenant_id:
              type: string
            generated_at:
              type: string
              format: date-time
              description: When the overview was computed (UTC).
            entities:
              type: integer
              description: Distinct live entities (after canonical resolution).
            triples:
              type: integer
              description: Live relations.
            communities:
              type: integer
              description: Detected communities.
            documents:
              type: integer
              description: Stored documents.
            unclustered_entities:
              type: integer
              description: Live entities that belong to no community.
        communities:
          type: array
          items:
            type: object
            required: [community_id, entity_count]
            properties:
              community_id:
                type: string
              name:
                type: ["string", "null"]
              summary:
                type: ["string", "null"]
              entity_count:
                type: integer
              top_entity_types:
                type: object
                additionalProperties:
                  type: integer
                description: The community's most common entity types with their counts.
        edges:
          type: array
          items:
            type: object
            required: [source, target, weight]
            properties:
              source:
                type: string
                description: Community identifier.
              target:
                type: string
                description: Community identifier.
              weight:
                type: integer
                description: Number of relations crossing between the two communities.
          description: Undirected inter-community edges.

    AtlasCommunity:
      type: object
      description: >-
        One community expanded into member entities and internal relations. Capped at 1,000
        nodes and 500 edges, strongest first.
      required: [community, nodes, edges]
      properties:
        community:
          type: object
          required: [community_id, entity_count]
          properties:
            community_id:
              type: string
            name:
              type: ["string", "null"]
            summary:
              type: ["string", "null"]
            entity_count:
              type: integer
        nodes:
          type: array
          items:
            type: object
            required: [id, name]
            properties:
              id:
                type: string
                description: Node identifier — the entity's canonical name.
              name:
                type: string
                description: Entity name.
              entity_type:
                type: ["string", "null"]
                description: The entity's most common type across its mentions.
              confidence:
                type: ["number", "null"]
                description: Best extraction confidence across the entity's mentions.
              mentions:
                type: integer
                description: Live mentions aggregated into this node.
              doc_count:
                type: integer
                description: Distinct source documents mentioning the entity.
        edges:
          type: array
          items:
            type: object
            required: [subject, predicate, object]
            properties:
              subject:
                type: string
              predicate:
                type: string
              object:
                type: string
              confidence:
                type: ["number", "null"]
                description: Best extraction confidence across occurrences of the relation.
              count:
                type: integer
                description: How many times the relation was extracted.
          description: Relations whose both endpoints belong to the community.

    AtlasEntity:
      type: object
      description: >-
        A dossier for one entity: aggregated profile, community membership, inbound and
        outbound relations, and evidence documents with short passage excerpts.
      required: [entity, communities, relations, evidence]
      properties:
        entity:
          type: object
          required: [name]
          properties:
            name:
              type: string
              description: Canonical entity name.
            entity_type:
              type: ["string", "null"]
              description: The entity's most common type across its mentions.
            aliases:
              type: ["array", "null"]
              items:
                type: string
              description: Known alternative names.
            confidence:
              type: ["number", "null"]
              description: Best extraction confidence across mentions.
            mentions:
              type: integer
              description: Live mentions aggregated into this profile.
            properties:
              type: ["object", "null"]
              description: Merged free-form attributes across mentions.
        communities:
          type: array
          items:
            type: object
            properties:
              community_id:
                type: string
              name:
                type: ["string", "null"]
          description: Communities the entity belongs to.
        relations:
          type: object
          description: The entity's relations, grouped by direction.
          properties:
            outbound:
              type: array
              items:
                type: object
                required: [predicate, object]
                properties:
                  predicate:
                    type: string
                  object:
                    type: string
                  confidence:
                    type: ["number", "null"]
                  count:
                    type: integer
                    description: How many times the relation was extracted.
            inbound:
              type: array
              items:
                type: object
                required: [predicate, subject]
                properties:
                  predicate:
                    type: string
                  subject:
                    type: string
                  confidence:
                    type: ["number", "null"]
                  count:
                    type: integer
                    description: How many times the relation was extracted.
        evidence:
          type: array
          items:
            type: object
            required: [document_id]
            properties:
              document_id:
                type: string
                description: Evidence document identifier.
              file_name:
                type: ["string", "null"]
                description: Original file name.
              source_id:
                type: ["string", "null"]
                description: Source the document came from.
              confidence:
                type: ["number", "null"]
                description: Best extraction confidence for the entity in this document.
              snippets:
                type: array
                items:
                  type: string
                description: Up to two short passage excerpts (300 characters) mentioning the entity.
          description: Up to 200 documents in which the entity was found, strongest first.
      additionalProperties: true
