openapi: 3.1.0
info:
  title: AISA v2.0 — Ablera.Serdica.AI.Support.Webservice HTTP API
  version: 1.0.0-draft
  description: |
    The HTTP contract the administration UI (serdica-ui `ai-support` section) and the client app consume — Delivery § 1.3
    track 3 "the UI needs the service's contracts, not its implementation". Realtime is SignalR (realtime-events.schema.json);
    this file is the request/response side. Every mutating call is a ledger event; every list is a projection over
    SRD_SUPPORT. Authentication: Authority JWT (audience SerdicaAPI); roles are ClaimTypes.Role claims (D134).
    Budgets are minutes (D135). No payload here ever carries an identifier — handles only (Trust and Data § 4).
    Base path on the QA host: /ai-support/api/v1 behind nginx.
  x-status: draft — stable shape, field-level extension expected during wave 1 (D139)
servers:
  - url: /ai-support/api/v1
security:
  - authorityJwt: []
tags:
  - { name: inbox,     description: Arrivals re-briefed from their source (UI › Inbox) }
  - { name: cases,     description: Cases, sessions, the case chat (UI › Cases) }
  - { name: decisions, description: Gates, decisions, held batches, write shapes (UI › Decisions) }
  - { name: papers,    description: The token-gated paper service }
  - { name: studio,    description: Profiles, prompts, skills, eval sets — draft → test → publish (UI › Studio) }
  - { name: knowledge, description: The memory tree, proposals, articles (UI › Knowledge) }
  - { name: metrics,   description: Ledger projections — hours and gates, never money (UI › Metrics) }
  - { name: control,   description: Connectors, roles and policies, exceptions, kill switch, audit tail (UI › Control) }
  - { name: client,    description: The customer's four screens }
paths:
  # ------------------------------------------------------------------ inbox
  /inbox/arrivals:
    get:
      tags: [inbox]
      summary: List arrivals (state new / awaiting_authorisation by default)
      parameters: [ { $ref: '#/components/parameters/state' }, { $ref: '#/components/parameters/page' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Page_Arrival' } } } } }
  /inbox/arrivals/{arrivalId}:
    get: { tags: [inbox], summary: "One arrival with its re-brief", parameters: [ { $ref: '#/components/parameters/arrivalId' } ], responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Arrival' } } } } } }
  /inbox/arrivals/{arrivalId}/decide:
    post:
      tags: [inbox]
      summary: One operator action — open, correct the route, merge into a case, dismiss
      description: Agents § 5c — one action corrects intake; the re-routing rate is a metric. Requires operator.
      parameters: [{$ref: '#/components/parameters/arrivalId'}, {$ref: '#/components/parameters/requestId'}]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ArrivalDecision' } } } }
      responses: {'200': {description: Decided, content: {application/json: {schema: {$ref: '#/components/schemas/Arrival'}}}}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}
  # ------------------------------------------------------------------ cases
  /cases:
    get:
      tags: [cases]
      summary: Case list — state, where, controller, waiting on, clocks
      parameters: [ { $ref: '#/components/parameters/state' }, { name: module, in: query, schema: { type: string, enum: [configuration, support, source] } }, { $ref: '#/components/parameters/page' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Page_CaseRow' } } } } }
    post:
      tags: [cases]
      summary: Open a case from the administration screen
      description: Intake still runs (routing, duplicate verdict, initiation). Requires operator; the module must be one the caller may start (intake.initiation).
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/OpenCaseRequest' } } } }
      responses: {'201': {description: Opened, content: {application/json: {schema: {$ref: '#/components/schemas/Case'}}}}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}
      parameters: [{$ref: '#/components/parameters/requestId'}]
  /cases/{caseId}:
    get: { tags: [cases], summary: "One case with its tasks, grants, clocks and papers index", parameters: [ { $ref: '#/components/parameters/caseId' } ], responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Case' } } } } } }
  /cases/{caseId}/cards:
    get:
      tags: [cases]
      summary: The case chat — one thread of cards, paged (ui.transcript page size 200)
      parameters: [ { $ref: '#/components/parameters/caseId' }, { name: after, in: query, schema: { type: integer, description: ledger SEQ_NO cursor } }, { name: limit, in: query, schema: { type: integer, maximum: 200, default: 200 } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Page_Card' } } } } }
  /cases/{caseId}/where:
    get: { tags: [cases], summary: "The Where rail — everything the case touched, by environment · system · object · mode", parameters: [ { $ref: '#/components/parameters/caseId' } ], responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/WhereChip' } } } } } } }
  /cases/{caseId}/control:
    post:
      tags: [cases]
      summary: Take control of the session (operator); the previous controller is notified
      parameters: [{$ref: '#/components/parameters/caseId'}, {$ref: '#/components/parameters/requestId'}]
      responses: {'200': {description: Controller changed, content: {application/json: {schema: {$ref: '#/components/schemas/Session'}}}}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}
  /cases/{caseId}/commands:
    post:
      tags: [cases]
      summary: A composer command — /steer /answer /stop /resume /handover /consult /park /try
      description: Only the controller may post; viewers have no composer (UI § 7). /handover raises Hd; /answer answers a parked question.
      parameters: [{$ref: '#/components/parameters/caseId'}, {$ref: '#/components/parameters/requestId'}]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Command' } } } }
      responses: {'202': {description: Accepted — the effect arrives as cards over SignalR}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}
  /cases/{caseId}/session:
    delete: {tags: [cases], summary: "Delete the live session (the case and its papers remain)", parameters: [{$ref: '#/components/parameters/caseId'}, {$ref: '#/components/parameters/requestId'}], responses: {'204': {description: Deleted}, '409': {$ref: '#/components/responses/Conflict'}}}
  # ------------------------------------------------------------------ decisions
  /decisions/gates:
    get:
      tags: [decisions]
      summary: Every waiting gate the caller may see, with its packet reference and age
      description: A gate the caller may decide shows actions; one they may not shows who can (Trust and Data § 3).
      parameters: [ { name: mine, in: query, schema: { type: boolean, default: true, description: only gates the caller's roles can decide } }, { $ref: '#/components/parameters/page' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Page_Gate' } } } } }
  /decisions/gates/{gateId}:
    get: { tags: [decisions], summary: "One gate with its decision packet (mechanism, verifier verdict, exact change with counts, revert, uncertainties, auditor verdict, the failed auto-confirm check)", parameters: [ { $ref: '#/components/parameters/gateId' } ], responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/GateDetail' } } } } } }
  /decisions/gates/{gateId}/decide:
    post:
      tags: [decisions]
      summary: Decide a gate — approve, reject, amend, defer
      description: |
        The caller must hold the gate's REQUIRED_ROLE for its target (approval is a permission, not a second person).
        Approving binds a grant to the shown artefact hash; a material change re-opens the gate. Round-trip target < 2 s.
      parameters: [{$ref: '#/components/parameters/gateId'}, {$ref: '#/components/parameters/requestId'}]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/GateDecisionRequest' } } } }
      responses: {'200': {description: Decided, content: {application/json: {schema: {$ref: '#/components/schemas/GateDecision'}}}}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}
  /decisions/held-batches:
    get: { tags: [decisions], summary: "Approved-but-unapplied work with its age", parameters: [ { $ref: '#/components/parameters/page' } ], responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Page_HeldBatch' } } } } } }
  /decisions/held-batches/{batchId}/apply:
    post: {tags: [decisions], summary: "Apply a held batch now (re-preflights if older than held_batches.repreflight_after)", parameters: [{name: batchId, in: path, required: true, schema: {type: string}}, {$ref: '#/components/parameters/requestId'}], responses: {'202': {description: Applying}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}}
  /decisions/shapes:
    get: { tags: [decisions], summary: "Write shapes being trained, approved, suspended — with clean-instance counts and expiry", parameters: [ { $ref: '#/components/parameters/state' } ], responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/WriteShapeRow' } } } } } } }
  /decisions/shapes/{shapeId}/extend:
    post:
      tags: [decisions]
      summary: Extend an approved shape's expiry — once, by at most one day (D133)
      parameters: [{name: shapeId, in: path, required: true, schema: {type: string}}, {$ref: '#/components/parameters/requestId'}]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [until], properties: { until: { type: string, format: date-time }, reason: { type: string } } } } } }
      responses: {'200': {description: Extended}, '403': {$ref: '#/components/responses/Forbidden'}, '422': {description: Beyond one day, or already extended — re-approval at HW-approve is the path: null}, '409': {$ref: '#/components/responses/Conflict'}}
  # ------------------------------------------------------------------ papers
  /papers/{caseId}:
    get: { tags: [papers], summary: "List the case's papers with revisions", parameters: [ { $ref: '#/components/parameters/caseId' } ], responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/PaperRef' } } } } } } }
  /papers/{caseId}/{kind}/{name}:
    get:
      tags: [papers]
      summary: Read a paper (latest or ?revision=n); every read is a PAPER_ACCESS ledger event
      parameters: [ { $ref: '#/components/parameters/caseId' }, { name: kind, in: path, required: true, schema: { type: string } }, { name: name, in: path, required: true, schema: { type: string } }, { name: revision, in: query, schema: { type: integer } } ]
      responses: { '200': { description: OK, content: { text/markdown: { schema: { type: string } }, application/json: { schema: {} }, application/octet-stream: { schema: { type: string, format: binary } } } } }
  # ------------------------------------------------------------------ studio
  /studio/{subject}:
    get:
      tags: [studio]
      summary: Versions of profiles | prompts | skills | eval-sets | routes with state and eval result
      parameters: [ { $ref: '#/components/parameters/subject' }, { name: key, in: query, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/GovernedVersion' } } } } } }
    post:
      tags: [studio]
      summary: Create a draft version (new, or "from template" — whole profiles, no inheritance, D50)
      parameters: [{$ref: '#/components/parameters/subject'}, {$ref: '#/components/parameters/requestId'}]
      requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
      responses: {'201': {description: Draft created, content: {application/json: {schema: {$ref: '#/components/schemas/GovernedVersion'}}}}, '409': {$ref: '#/components/responses/Conflict'}}
  /studio/{subject}/{key}/{version}/try:
    post: {tags: [studio], summary: "Try a draft on one case (canary) — no publish", parameters: [{$ref: '#/components/parameters/subject'}, {$ref: '#/components/parameters/key'}, {$ref: '#/components/parameters/version'}, {$ref: '#/components/parameters/requestId'}], requestBody: {content: {application/json: {schema: {type: object, properties: {case_id: {type: string}}}}}}, responses: {'202': {description: Running}, '409': {$ref: '#/components/responses/Conflict'}}}
  /studio/{subject}/{key}/{version}/evaluate:
    post: {tags: [studio], summary: "Run the eval set; the run is an EVAL_RUNS row and a GOVERNANCE_EVENTS evaluated/PASSED|FAILED event", parameters: [{$ref: '#/components/parameters/subject'}, {$ref: '#/components/parameters/key'}, {$ref: '#/components/parameters/version'}, {$ref: '#/components/parameters/requestId'}], responses: {'202': {description: Running, content: {application/json: {schema: {type: object, properties: {eval_run_id: {type: string}}}}}}, '409': {$ref: '#/components/responses/Conflict'}}}
  /studio/{subject}/{key}/{version}/approve:
    post: {tags: [studio], summary: "Approve (prompt_publisher for the module/stage) — a GOVERNANCE_EVENTS approved event", parameters: [{$ref: '#/components/parameters/subject'}, {$ref: '#/components/parameters/key'}, {$ref: '#/components/parameters/version'}, {$ref: '#/components/parameters/requestId'}], responses: {'200': {description: Approved}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}}
  /studio/{subject}/{key}/{version}/publish:
    post: {tags: [studio], summary: "Publish — refused by the database without a PASSED evaluation and an approval, or with a red eval set (008 guards)", parameters: [{$ref: '#/components/parameters/subject'}, {$ref: '#/components/parameters/key'}, {$ref: '#/components/parameters/version'}, {$ref: '#/components/parameters/requestId'}], responses: {'200': {description: Published}, '403': {$ref: '#/components/responses/Forbidden'}, '422': {description: Guard refused — the body names the failing condition}, '409': {$ref: '#/components/responses/Conflict'}}}
  /studio/{subject}/{key}/rollback:
    post: {tags: [studio], summary: "Roll back = re-publish the previous version; nothing is edited in place", parameters: [{$ref: '#/components/parameters/subject'}, {$ref: '#/components/parameters/key'}, {$ref: '#/components/parameters/requestId'}], requestBody: {content: {application/json: {schema: {type: object, required: [to_version], properties: {to_version: {type: integer}}}}}}, responses: {'200': {description: Rolled back}, '409': {$ref: '#/components/responses/Conflict'}}}
  # ------------------------------------------------------------------ knowledge
  /knowledge/tree:
    get: { tags: [knowledge], summary: "The memory tree — nodes, owners, article counts", responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/MemoryNode' } } } } } } }
  /knowledge/proposals:
    get: { tags: [knowledge], summary: "The proposals queue (state proposed) with provenance and the cases that used each", responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Article' } } } } } } }
  /knowledge/articles/{articleId}/confirm:
    post: {tags: [knowledge], summary: "Confirm a proposal into active (human review) — the owning profile's curator or an operator of that domain", parameters: [{name: articleId, in: path, required: true, schema: {type: string}}, {$ref: '#/components/parameters/requestId'}], responses: {'200': {description: Active}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}}
  /knowledge/articles/{articleId}/retire:
    post: {tags: [knowledge], summary: "Retire an article; the response lists the cases that pinned it", parameters: [{name: articleId, in: path, required: true, schema: {type: string}}, {$ref: '#/components/parameters/requestId'}], requestBody: {content: {application/json: {schema: {type: object, required: [reason], properties: {reason: {type: string}}}}}}, responses: {'200': {description: Retired, content: {application/json: {schema: {type: object, properties: {pinned_by_cases: {type: array, items: {type: string}}}}}}}, '409': {$ref: '#/components/responses/Conflict'}}}
  /knowledge/experience/search:
    get: { tags: [knowledge], summary: "Symptom-signature lookup over the experience index (the triage precedent query)", parameters: [ { name: q, in: query, required: true, schema: { type: string } }, { name: node, in: query, schema: { type: string, enum: [support/experience, platform/experience] } } ], responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ExperienceHit' } } } } } } }
  # ------------------------------------------------------------------ metrics
  /metrics/{metric}:
    get:
      tags: [metrics]
      summary: One metric of Metrics § targets, parsed from the ledger by a tool — hours and gates, never money
      parameters: [ { name: metric, in: path, required: true, schema: { type: string } }, { name: from, in: query, schema: { type: string, format: date } }, { name: to, in: query, schema: { type: string, format: date } }, { name: module, in: query, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/MetricSeries' } } } } }
  # ------------------------------------------------------------------ control
  /control/connectors:
    get: { tags: [control], summary: "Connector scopes — reach, operations, ceilings — with health per environment", responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ConnectorScopeRow' } } } } } } }
  /control/connectors/{scopeId}/probe:
    post: {tags: [control], summary: "Probe a reach row now; the result updates VERIFIED_ON_UTC and health (administrator)", parameters: [{name: scopeId, in: path, required: true, schema: {type: string}}, {$ref: '#/components/parameters/requestId'}], responses: {'200': {description: Probed}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}}
  /control/policies:
    get: { tags: [control], summary: "Policy values by scope and key (the Whitelabel Catalogue as rows)", parameters: [ { name: scope, in: query, schema: { type: string, enum: [platform, customer, case_type, profile] } }, { name: key, in: query, schema: { type: string } } ], responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/PolicyValue' } } } } } } }
    post: {tags: [control], summary: "Publish a new version of a policy value (administrator; customer scope = a governed change of the plug-in)", requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/PolicyValue'}}}}, responses: {'201': {description: Published}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}, parameters: [{$ref: '#/components/parameters/requestId'}]}
  /control/roles:
    get: { tags: [control], summary: "Live role assignments (token_claim · directory_group · platform_row) per subject", responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/RoleAssignment' } } } } } } }
  /control/risk-acceptances:
    get: { tags: [control], summary: "The N-12 register — accepted risk classes per case type × gate × target", responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/RiskAcceptance' } } } } } } }
    post: {tags: [control], summary: "Record an organisational acceptance (administrator, with the decision reference) — never a policy row, never an agent", requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/RiskAcceptance'}}}}, responses: {'201': {description: Recorded}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}, parameters: [{$ref: '#/components/parameters/requestId'}]}
  /control/exceptions:
    get: { tags: [control], summary: "Parked cases by reason with age, suspended shapes, red connectors, budget stops", responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Exceptions' } } } } } }
  /control/kill-switch:
    get: {tags: [control], summary: 'Current live mode, checked at case admission and every effect dispatch.', responses: {'200': {description: OK, content: {application/json: {schema: {type: object, properties: {mode: {type: string, enum: ['off', soft, hard]}, since: {type: string, format: date-time}}}}}}}}
    post: {tags: [control], summary: "Set live off/soft/hard mode (Administrator); publish control event after the state commit.", parameters: [{$ref: '#/components/parameters/requestId'}], requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: false, required: [mode, expected_version, reason], properties: {mode: {type: string, enum: ['off', soft, hard]}, expected_version: {type: string}, reason: {type: string, minLength: 1}}}}}}, responses: {'200': {description: Live state changed and recorded.}, '403': {$ref: '#/components/responses/Forbidden'}, '409': {$ref: '#/components/responses/Conflict'}}}
  /control/audit:
    get: { tags: [control], summary: "The audit tail — ledger records, filterable by case, kind, actor, time", parameters: [ { name: case_id, in: query, schema: { type: string } }, { name: kind, in: query, schema: { type: string } }, { name: after_seq, in: query, schema: { type: integer } }, { name: limit, in: query, schema: { type: integer, maximum: 500 } } ], responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: 'ledger-record.schema.json' } } } } } } }
  /control/ledger/verify:
    post: {tags: [control], summary: "Run verify-ledger (chain continuity, seal integrity, export currency) and return the result", responses: {'200': {description: OK, content: {application/json: {schema: {type: object, properties: {ok: {type: boolean}, head_seq_no: {type: integer}, head_hash: {type: string}, last_exported_seal: {type: integer}, findings: {type: array, items: {type: string}}}}}}}, '409': {$ref: '#/components/responses/Conflict'}}, parameters: [{$ref: '#/components/parameters/requestId'}]}
  # ------------------------------------------------------------------ client (the customer's four screens)
  /client/requests:
    get: { tags: [client], summary: "My requests — is anyone waiting on me (customer_representative)", responses: { '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/ClientRequest' } } } } } } }
    post: {tags: [client], summary: "Start an authorised intake case automatically; classification and destination entitlement checks follow.", requestBody: {required: true, content: {application/json: {schema: {type: object, required: [module, description], properties: {module: {type: string, enum: [support, configuration]}, description: {type: string}, attachments: {type: array, items: {type: string}}}}}}}, responses: {'201': {description: Opened}, '202': {description: Waiting for your organisation's approval}, '403': {description: Not granted — with the contract clause and who to talk to}, '409': {$ref: '#/components/responses/Conflict'}}, parameters: [{$ref: '#/components/parameters/requestId'}]}
  /client/requests/{caseId}:
    get: { tags: [client], summary: "One request — what was asked, what has been established, the single open question; no transcript, no system names, no handles", parameters: [ { $ref: '#/components/parameters/caseId' } ], responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ClientRequest' } } } } } }
  /client/requests/{caseId}/accept:
    post: {tags: [client], summary: "Customer-owned H1/H3 business confirmation or CG-11 preview/delivery acceptance, recorded with the person's name and the artefact hash", parameters: [{$ref: '#/components/parameters/caseId'}, {$ref: '#/components/parameters/requestId'}], requestBody: {required: true, content: {application/json: {schema: {type: object, required: [gate_id, artefact_hash, decision, scope, expected_version], properties: {gate_id: {type: string}, artefact_hash: {type: string}, decision: {type: string, enum: [accept, reject]}, note: {type: string}, scope: {type: string, enum: [scope, stage_result, preview, delivery]}, expected_version: {type: string}}}}}}, responses: {'200': {description: Recorded}, '409': {$ref: '#/components/responses/Conflict'}}}
  /client/requests/{caseId}/responses:
    post:
      tags: [client]
      summary: Answer a question or report an assigned action; queue verification, never accept delivery implicitly
      description: Authenticate the designated respondent and customer/case ownership. Compare the current question/step, request reference and shown version. Scan attachments and substitute handles. Commit the response, INBOX_MESSAGES dedup record and verification outbox message atomically. Same key/body returns the same acknowledgement; stale scope or a response for another actor is rejected. No platform WRITE_LOG is invented for external work.
      parameters: [{$ref: '#/components/parameters/caseId'}, {$ref: '#/components/parameters/requestId'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CaseResponse'}}}}
      responses:
        '202': {description: Response recorded; verification or eligible question work queued.}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
  /cases/{caseId}/responses:
    post:
      tags: [cases]
      summary: Record an operator answer or attributable external evidence through the same response service
      description: Require case control or the assigned response role. Imported channel evidence retains its source actor and request reference; the operator is recorded as recorder, not as executor. Scope, attachment and dedup checks are identical to client responses.
      parameters: [{$ref: '#/components/parameters/caseId'}, {$ref: '#/components/parameters/requestId'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CaseResponse'}}}}
      responses:
        '202': {description: Response recorded and eligible work queued.}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
components:
  securitySchemes:
    authorityJwt: { type: http, scheme: bearer, bearerFormat: JWT, description: Authority (OIDC) access token, audience SerdicaAPI; roles as ClaimTypes.Role claims }
  parameters:
    page: { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
    state: { name: state, in: query, schema: { type: string } }
    caseId: { name: caseId, in: path, required: true, schema: { type: string } }
    arrivalId: { name: arrivalId, in: path, required: true, schema: { type: string } }
    gateId: { name: gateId, in: path, required: true, schema: { type: string } }
    subject: { name: subject, in: path, required: true, schema: { type: string, enum: [profiles, prompts, skills, eval-sets, routes] } }
    key: { name: key, in: path, required: true, schema: { type: string } }
    version: { name: version, in: path, required: true, schema: { type: integer } }
    requestId: {name: Idempotency-Key, in: header, required: true, schema: {type: string, minLength: 8, maxLength: 128}, description: 'Unique logical command id, scoped server-side to authenticated caller and operation. Same key/different body is a conflict.'}
  responses:
    Forbidden: { description: The caller does not hold the role for this action and target; the body names who does, content: { application/json: { schema: { type: object, properties: { required_role: { type: string }, holders: { type: array, items: { type: string } } } } } } }
    Conflict: {description: 'Stale version, changed packet, already decided resource or idempotency-key/body mismatch; reload the resource before a new command.'}
  schemas:
    WhereChip:
      type: object
      required: [environment, system, object, mode]
      properties: { environment: { type: string }, system: { type: string }, object: { type: string }, mode: { type: string, enum: [read, write, send, ddl, source] }, resolved: { type: boolean } }
    Clock:
      type: object
      properties: { kind: { type: string, enum: [response, resolution, offer] }, due: { type: string, format: date-time }, paused_since: { type: string, format: date-time }, breach_warning: { type: boolean } }
    Budget:
      type: object
      description: minutes only (D135)
      properties: { predicted_minutes: { type: number }, used_minutes: { type: number }, soft_warning_at_pct: { type: integer, default: 80 }, state: { type: string, enum: [under, warned, stopped] } }
    Arrival:
      type: object
      properties: { id: { type: string }, channel: { type: string }, origin_key: { type: string }, received_on: { type: string, format: date-time }, subject: { type: string }, brief_paper_ref: { type: string }, identifiers: { type: array, items: { $ref: '#/components/schemas/WhereChip' } }, duplicate_of_case_id: { type: string }, proposed_module: { type: string }, proposed_case_type: { type: string }, route_confidence: { type: string, enum: [high, low] }, initiation_outcome: { type: string }, clocks: { type: array, items: { $ref: '#/components/schemas/Clock' } }, precedents: { type: array, items: { type: string } }, state: { type: string } }
    ArrivalDecision:
      type: object
      required: [action]
      properties: { action: { type: string, enum: [open, reroute, merge, dismiss] }, module: { type: string }, case_type: { type: string }, merge_into_case_id: { type: string }, reason: { type: string } }
    OpenCaseRequest:
      type: object
      required: [module, objective]
      properties: { module: { type: string, enum: [configuration, support, source] }, case_type: { type: string }, objective: { type: string }, customer_code: { type: string }, attachments: { type: array, items: { type: string } }, links: { type: array, items: { type: object, properties: { system: { type: string }, ref: { type: string } } } } }
    CaseRow:
      type: object
      properties: {id: {type: string}, case_no: {type: string}, module: {type: string}, case_type: {type: string}, state: {type: string}, park_reason: {type: string}, where: {type: array, items: {$ref: '#/components/schemas/WhereChip'}}, controller: {type: string}, viewers: {type: integer}, waiting_on: {type: string}, clocks: {type: array, items: {$ref: '#/components/schemas/Clock'}}, budget: {$ref: '#/components/schemas/Budget'}, sub_case_age_minutes: {type: number}, opened_on: {type: string, format: date-time}, row_version: {type: string, description: 'Opaque decimal concurrency token; compare on commands, do not increment in the browser.'}, working_module: {type: string, description: Business graph id; data is embedded in persisted module support.}}
    Case:
      allOf:
        - $ref: '#/components/schemas/CaseRow'
        - type: object
          properties: { objective: { type: string }, session: { $ref: '#/components/schemas/Session' }, tasks: { type: array, items: { $ref: '#/components/schemas/TaskRow' } }, grants: { type: array, items: { $ref: '#/components/schemas/GrantRow' } }, papers: { type: array, items: { $ref: '#/components/schemas/PaperRef' } }, links: { type: array, items: { type: object, properties: { kind: { type: string }, system: { type: string }, ref: { type: string }, authoritative: { type: boolean } } } }, parent_case_id: { type: string }, sub_cases: { type: array, items: { type: string } } }
    Session:
      type: object
      properties: {id: {type: string}, state: {type: string, enum: [live, stopped, resumed, deleted]}, controller: {type: string}, viewers: {type: integer}, worker_id: {type: string}, last_checkpoint: {type: string, format: date-time}, row_version: {type: string, description: 'Opaque decimal concurrency token; compare on commands, do not increment in the browser.'}}
    TaskRow:
      type: object
      properties: {id: {type: string}, task_path: {type: string}, kind: {type: string}, profile_key: {type: string}, profile_version: {type: integer}, stage_id: {type: string}, state: {type: string}, park_reason: {type: string}, budget: {$ref: '#/components/schemas/Budget'}, row_version: {type: string, description: 'Opaque decimal concurrency token; compare on commands, do not increment in the browser.'}}
    GrantRow:
      type: object
      properties: { id: { type: string }, environment: { type: string }, system: { type: string }, target: { type: string }, mode: { type: string }, bound_artefact_hash: { type: string }, expires_on: { type: string, format: date-time }, state: { type: string } }
    Card:
      type: object
      description: UI § 2 — one card of the case chat; every card says where it is about
      required: [seq_no, kind, occurred_on, actor]
      properties:
        seq_no: { type: integer, description: ledger SEQ_NO — the cursor }
        kind: { type: string, enum: [brief, agent, tool, question, plan, decision, verdict, write, handover, consult, status, human, memory] }
        occurred_on: { type: string, format: date-time }
        actor: { type: object, properties: { kind: { type: string }, id: { type: string }, profile_key: { type: string }, profile_version: { type: integer } } }
        task_path: { type: string }
        where: { $ref: '#/components/schemas/WhereChip' }
        title: { type: string }
        stated_reasoning: { type: string, description: the agent's declared reasoning for a step — never raw model chain-of-thought }
        summary: { type: string }
        paper_refs: { type: array, items: { type: string } }
        gate_id: { type: string }
        trust_class: { type: string }
        ledger_record_id: { type: string }
    Command:
      type: object
      required: [command]
      properties: { command: { type: string, enum: [steer, answer, stop, resume, handover, consult, park, try, where, papers] }, text: { type: string }, question_id: { type: string }, to_module: { type: string }, domain: { type: string } }
    Gate:
      type: object
      properties: {id: {type: string}, case_id: {type: string}, case_no: {type: string}, kind: {type: string}, target_class: {type: string}, where: {$ref: '#/components/schemas/WhereChip'}, required_role: {type: string}, can_decide: {type: boolean}, holders: {type: array, items: {type: string}}, opened_on: {type: string, format: date-time}, age_minutes: {type: number}, escalation_level: {type: integer}, auto_confirm_failed_check: {type: integer}, auditor_verdict: {type: string}, row_version: {type: string, description: 'Opaque decimal concurrency token; compare on commands, do not increment in the browser.'}}
    GateDetail:
      allOf:
        - $ref: '#/components/schemas/Gate'
        - type: object
          properties: { gate_id: { type: string }, artefact_hash: { type: string }, packet_paper_ref: { type: string }, packet: { description: 'the decision packet (solution packet / write packet / brief / sign-out) — read from the paper', type: object }, auditor_verdict_paper_ref: { type: string }, revert_path_ref: { type: string } }
    GateDecisionRequest:
      type: object
      required: [decision, artefact_hash, expected_version]
      properties: {decision: {type: string, enum: [approve, reject, amend, defer, accept]}, artefact_hash: {type: string, description: must equal the packet shown — otherwise 409}, scope_approved: {type: object}, reason: {type: string}, expected_version: {type: string}}
    GateDecision:
      type: object
      properties: { id: { type: string }, gate_id: { type: string }, actor_kind: { type: string, enum: [person, policy] }, actor_id: { type: string }, decision: { type: string }, decided_on: { type: string, format: date-time }, grant_ids: { type: array, items: { type: string } }, ledger_record_id: { type: string } }
    HeldBatch:
      type: object
      properties: { id: { type: string }, case_id: { type: string }, items_total: { type: integer }, items_applied: { type: integer }, state: { type: string }, held_since: { type: string, format: date-time }, age_minutes: { type: number }, needs_repreflight: { type: boolean } }
    WriteShapeRow:
      type: object
      properties: { id: { type: string }, shape_key: { type: string }, version: { type: integer }, module: { type: string }, write_class: { type: string }, target_class: { type: string }, state: { type: string }, clean_instances: { type: integer }, approved_on: { type: string, format: date-time }, expires_on: { type: string, format: date-time }, extended_until: { type: string, format: date-time }, suspended_reason: { type: string } }
    PaperRef:
      type: object
      properties: { id: { type: string }, kind: { type: string }, name: { type: string }, revision_no: { type: integer }, content_hash: { type: string }, media_type: { type: string }, byte_length: { type: integer }, created_on: { type: string, format: date-time } }
    GovernedVersion:
      type: object
      properties: {subject: {type: string}, key: {type: string}, version: {type: integer}, state: {type: string, enum: [draft, testing, published, retired]}, owner: {type: string}, eval_result: {type: string, enum: [green, red, unknown]}, last_eval_run_id: {type: string}, published_on: {type: string, format: date-time}, events: {type: array, items: {type: object, properties: {event: {type: string}, result: {type: string}, actor: {type: string}, true: {type: string, format: date-time}}}}, row_version: {type: string, description: 'Opaque decimal concurrency token; compare on commands, do not increment in the browser.'}}
    MemoryNode:
      type: object
      properties: { node_path: { type: string }, parent_path: { type: string }, branch: { type: string }, owner_profile_key: { type: string }, article_count: { type: integer }, index_commit_sha: { type: string }, refreshed_on: { type: string, format: date-time } }
    Article:
      type: object
      properties: { id: { type: string }, node_path: { type: string }, article_key: { type: string }, version: { type: integer }, title: { type: string }, state: { type: string }, provenance: { type: object }, reaches_untrusted: { type: boolean }, used_by_cases: { type: array, items: { type: string } }, contradicted: { type: boolean }, git_sha: { type: string } }
    ExperienceHit:
      type: object
      properties: { id: { type: string }, symptom_signature: { type: string }, mechanism: { type: string }, fix_shape: { type: string }, skill_key: { type: string }, case_id: { type: string }, observed_version: { type: string }, score: { type: number } }
    MetricSeries:
      type: object
      properties: { metric: { type: string }, unit: { type: string, enum: [minutes, hours, count, percent, days] }, basis: { type: string, description: the ledger query that produced it }, target: { type: number }, baseline: { type: number }, points: { type: array, items: { type: object, properties: { period: { type: string }, value: { type: number } } } } }
    ConnectorScopeRow:
      allOf:
        - $ref: 'connector-capability.schema.json'
        - type: object
          properties: { id: { type: string }, health: { type: string }, verified_on: { type: string, format: date-time } }
    PolicyValue:
      type: object
      required: [scope, key, value]
      properties: { id: { type: string }, scope: { type: string, enum: [platform, customer, case_type, profile] }, customer_code: { type: string }, module: { type: string }, case_type: { type: string }, profile_key: { type: string }, key: { type: string }, version: { type: integer }, value: {}, value_type: { type: string }, state: { type: string } }
    RoleAssignment:
      type: object
      properties: { subject_id: { type: string }, role: { type: string, enum: [viewer, operator, approver, prompt_publisher, administrator, customer_representative] }, scope_module: { type: string }, scope_stage: { type: string }, scope_target_class: { type: string }, source: { type: string, enum: [token_claim, directory_group, platform_row] }, granted_on: { type: string, format: date-time } }
    RiskAcceptance:
      type: object
      required: [case_type, gate_kind, target_class, write_class, decision_ref]
      properties: {id: {type: string}, customer_code: {type: string}, module: {type: string}, case_type: {type: string}, gate_kind: {type: string}, target_class: {type: string}, write_class: {type: string, enum: [W1, W2, W3, W4, W5, W6, W7]}, accepted_by: {type: string}, decision_ref: {type: string}, accepted_on: {type: string, format: date-time}, withdrawn_on: {type: string, format: date-time}}
    Exceptions:
      type: object
      properties: { parked: { type: array, items: { type: object, properties: { case_id: { type: string }, reason: { type: string }, age_minutes: { type: number } } } }, suspended_shapes: { type: array, items: { $ref: '#/components/schemas/WriteShapeRow' } }, red_connectors: { type: array, items: { type: object, properties: { connector: { type: string }, environment: { type: string }, since: { type: string, format: date-time } } } }, budget_stops: { type: array, items: { type: string } }, ledger_export_lag_minutes: { type: number } }
    CaseResponse:
      type: object
      additionalProperties: false
      required: [request_ref, expected_version, response_kind, text, evidence_refs]
      properties:
        request_ref: {type: string, minLength: 1, description: Exact outstanding question or external instruction paper.}
        expected_version: {type: string, minLength: 1}
        response_kind: {type: string, enum: [answer, report_completed, cannot_perform]}
        question_id: {type: string, minLength: 1}
        step_id: {type: string, minLength: 1}
        plan_revision: {type: integer, minimum: 1}
        text: {type: string, minLength: 1}
        evidence_refs: {type: array, items: {type: string, minLength: 1}, uniqueItems: true, description: Existing case-scoped scanned evidence; presence is not a verification verdict.}
      oneOf:
        - properties: {response_kind: {const: answer}}
          required: [question_id]
        - properties: {response_kind: {enum: [report_completed, cannot_perform]}}
          required: [step_id, plan_revision]
    PendingAction:
      type: object
      required: [step_id, plan_revision, request_ref, instruction, waiting_on, requested_on, evidence_needed]
      properties:
        step_id: {type: string}
        plan_revision: {type: integer}
        request_ref: {type: string}
        instruction: {type: string}
        waiting_on: {type: string, description: Customer-facing actor or organisation label.}
        requested_on: {type: string, format: date-time}
        evidence_needed: {type: array, items: {type: string}}
    ClientRequest:
      type: object
      description: the customer's vocabulary only — no transcript, no environment/system/agent names, no handles, no figures about cost
      properties: {row_version: {type: string}, pending_action: {$ref: '#/components/schemas/PendingAction'}, id: {type: string}, title: {type: string}, state: {type: string, enum: [received, in_progress, waiting_for_you, ready_for_acceptance, done, declined]}, what_was_asked: {type: string}, what_is_established: {type: string}, open_question: {type: string}, waiting_on_you: {type: boolean}, acceptance: {type: object, properties: {gate_id: {type: string}, artefact_hash: {type: string}, summary: {type: string}, scope: {type: string, enum: [scope, stage_result, preview, delivery]}, target_label: {type: string, description: Customer-facing label for the exact preview or delivered target.}}}, updated_on: {type: string, format: date-time}}
    Page_Arrival: { type: object, properties: { items: { type: array, items: { $ref: '#/components/schemas/Arrival' } }, page: { type: integer }, total: { type: integer } } }
    Page_CaseRow: { type: object, properties: { items: { type: array, items: { $ref: '#/components/schemas/CaseRow' } }, page: { type: integer }, total: { type: integer } } }
    Page_Card: { type: object, properties: { items: { type: array, items: { $ref: '#/components/schemas/Card' } }, next_after: { type: integer } } }
    Page_Gate: { type: object, properties: { items: { type: array, items: { $ref: '#/components/schemas/Gate' } }, page: { type: integer }, total: { type: integer } } }
    Page_HeldBatch: { type: object, properties: { items: { type: array, items: { $ref: '#/components/schemas/HeldBatch' } }, page: { type: integer }, total: { type: integer } } }

x-command-protocol: {authority: Reload roles and resource ownership before applying or replaying a command result., idempotency: 'Commit INBOX_MESSAGES command key/hash, state transition and OUTBOX_MESSAGES together.', concurrency: Existing-resource decisions compare the shown row_version and artefact hash; preview acceptance does not complete a requested release.}
