{"openapi":"3.1.0","info":{"title":"Api","version":"0.1.0","description":"Homium Compliance Portal API — expected document lists, versions, saved sections, applications"},"servers":[{"url":"/api","description":"Base API path"}],"security":[{"cookie":[]},{"bearer":[]}],"tags":[{"name":"health","description":"Health operations"},{"name":"access","description":"The access matrix as the server enforces it"},{"name":"keys","description":"API keys — an organization's machine identity (Managers only)"},{"name":"webhooks","description":"Outbound webhooks fed by the ledger (Managers only; off until one is added)"},{"name":"users","description":"People, sign-in, sessions and passwords"},{"name":"contact","description":"The marketing site's contact form (public)"},{"name":"expected-doc-list-library","description":"Expected document list families, versions, lifecycle"},{"name":"expected-doc-list-editor","description":"Reading and saving a single expected document list version"},{"name":"saved-sections","description":"Reusable section fragments (copies on insert)"},{"name":"bundle-catalogue","description":"Shared document bundles an expected document list inserts as a subsection"},{"name":"doc-type-extraction-schemas","description":"Document schemas — the fields extraction pulls per document type"},{"name":"workflow-library","description":"Workflows — named, versioned run recipes, and the step palette"},{"name":"intel-library","description":"Intel — named, versioned sets of research questions answered over a whole case"},{"name":"intel-briefs","description":"Intel briefs — one template's questions answered over one case, every claim cited to its document, page and line"},{"name":"intake","description":"Applications filling a pinned expected document list version"},{"name":"files","description":"Source files — every byte that enters lands here as an immutable record"},{"name":"sets","description":"Set expected documents — the instances a case holds"},{"name":"approved-docs","description":"The approval trail and the filed documents it produces"},{"name":"documents","description":"What an application holds, across runs"},{"name":"analysis","description":"Runs as the analyzer landed them, and the worker's ingest"},{"name":"runs","description":"The run lifecycle — gate, preview, parse, tools"},{"name":"models","description":"Model options per stage"},{"name":"ledger","description":"The append-only audit trail per application"},{"name":"agent","description":"The case assistant's helpers"},{"name":"agent-stream","description":"The case assistant's turn as a server-sent event stream (not generated)"}],"paths":{"/healthz":{"get":{"operationId":"healthCheck","security":[],"tags":["health"],"summary":"Health check","description":"Returns server health status","responses":{"200":{"description":"Healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthStatus"}}}}}}},"/expected-doc-lists":{"get":{"operationId":"listExpectedDocLists","tags":["expected-doc-list-library"],"summary":"Library index derived from the expected document list files on disk","responses":{"200":{"description":"All expected document list versions grouped client-side by expected document list id","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExpectedDocListListing"}}}}}}},"post":{"operationId":"createExpectedDocList","tags":["expected-doc-list-library"],"summary":"Create a new expected document list as a v1 draft (blank scaffold)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocListInput"}}}},"responses":{"201":{"description":"Created draft reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocListRef"}}}},"409":{"description":"Family already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/expected-doc-lists/{expectedDocListId}/{version}":{"get":{"operationId":"getExpectedDocList","tags":["expected-doc-list-editor"],"summary":"Full expected document list JSON for one version","parameters":[{"$ref":"#/components/parameters/ExpectedDocListId"},{"$ref":"#/components/parameters/Version"}],"responses":{"200":{"description":"The expected document list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocList"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}},"put":{"operationId":"saveExpectedDocList","tags":["expected-doc-list-editor"],"summary":"Save a draft (a draft is the only writable status)","parameters":[{"$ref":"#/components/parameters/ExpectedDocListId"},{"$ref":"#/components/parameters/Version"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocList"}}}},"responses":{"200":{"description":"Saved expected document list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocList"}}}},"400":{"description":"Contract validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"Version is active or superseded, and immutable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/expected-doc-lists/{expectedDocListId}/{version}/new-version":{"post":{"operationId":"createNewVersion","tags":["expected-doc-list-library"],"summary":"Copy vN to v(N+1) as a draft; vN stays active — or copy an official list template as the caller's own","description":"On the caller's own list: vN → v(N+1) draft. On an official list template (`expectedDocListCatalogOfficial`, read-only): the caller's own copy, version 1 draft, kind `expectedDocListCatalogOrgCustom`, under the same id — 409 when the caller already has a list with that id. On an official document catalogue: 409 — an organization overrides an entry through its own `custom-documents` instead.\n","parameters":[{"$ref":"#/components/parameters/ExpectedDocListId"},{"$ref":"#/components/parameters/Version"}],"responses":{"201":{"description":"The new draft reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocListRef"}}}},"409":{"description":"A newer version already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/expected-doc-lists/{expectedDocListId}/{version}/rename-as-new-version":{"post":{"operationId":"renameExpectedDocListAsNewVersion","tags":["expected-doc-list-library"],"summary":"Copy vN to v(N+1) as a draft under a new name; vN keeps its own","description":"How an active version is renamed. The version itself never changes name — applications are pinned to it under the one it was activated with — so the new name arrives on the version after it. Branch and name are a single write: a failure leaves no half-renamed copy behind.\n","parameters":[{"$ref":"#/components/parameters/ExpectedDocListId"},{"$ref":"#/components/parameters/Version"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameInput"}}}},"responses":{"201":{"description":"The new draft reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocListRef"}}}},"409":{"description":"A newer version already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/expected-doc-lists/{expectedDocListId}/{version}/duplicate":{"post":{"operationId":"duplicateExpectedDocList","tags":["expected-doc-list-library"],"summary":"Copy this version into a brand-new expected document list as v1 draft","parameters":[{"$ref":"#/components/parameters/ExpectedDocListId"},{"$ref":"#/components/parameters/Version"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocListDuplicateInput"}}}},"responses":{"201":{"description":"New expected document list reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocListRef"}}}},"409":{"description":"Target expected document list id already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/expected-doc-lists/{expectedDocListId}/{version}/activate":{"post":{"operationId":"activateExpectedDocList","tags":["expected-doc-list-library"],"summary":"Activate a draft; the active version it replaces is superseded in the same write","parameters":[{"$ref":"#/components/parameters/ExpectedDocListId"},{"$ref":"#/components/parameters/Version"}],"responses":{"200":{"description":"Updated listing row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocListListing"}}}},"409":{"description":"Not a draft — already active or superseded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/expected-doc-lists/{expectedDocListId}/{version}/set-default":{"post":{"operationId":"setDefaultExpectedDocList","tags":["expected-doc-list-library"],"summary":"Make this list the organization's default — the one the new-application form pre-selects","description":"Called on the list's active version; the whole list (every version) becomes the default, and every other list stops being it, in one write.\n","parameters":[{"$ref":"#/components/parameters/ExpectedDocListId"},{"$ref":"#/components/parameters/Version"}],"responses":{"200":{"description":"The library after the change","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExpectedDocListListing"}}}}},"404":{"description":"No such list version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"The version is not active, so its list cannot be the default","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/doc-type-catalog/marked":{"get":{"operationId":"getMarkedDocTypeCatalog","tags":["expected-doc-list-library"],"summary":"The organization's marked official document catalogue, resolved down its chain","description":"The chain from the marked catalogue to `generic`, the official organization's latest active version of each, merged root-first so the more specific entry wins by `docType`. Every entry is an expected document; the root's entries are the docKinds. What the picker, the type select, the list editor and the run request's kinds payload read.\n","responses":{"200":{"description":"The marked catalogue's entries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkedDocTypeCatalog"}}}}}}},"/doc-types":{"post":{"operationId":"coinDocType","tags":["expected-doc-list-library"],"summary":"Coin a document type into the organization's custom catalogue","description":"The id is the slug of the name. A document of the marked chain (official or the organization's own) with that id already means it and is answered as it is (200); otherwise the entry is appended to the organization's `docTypeCatalogOrgCustom` list (`custom-documents`, created on first use) with the given kind and the requirement defaults (201). The picker on the Classify and Extract pages coins through this and nothing else.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoinDocTypeInput"}}}},"responses":{"200":{"description":"A document with this id already exists in the marked chain — nothing was written","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDoc"}}}},"201":{"description":"The coined entry, now in the organization's custom catalogue","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDoc"}}}},"400":{"description":"The name yields no usable id, the id is reserved, or the kind is not a root entry of the marked chain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/document-types":{"get":{"operationId":"listDocumentTypes","tags":["doc-type-extraction-schemas"],"summary":"The Extract page — every document of the marked chain with its extraction schema versions","description":"One row per document of the marked chain (the official catalogues down to `generic`, the organization's `docTypeCatalogOrgCustom` laid over; page kinds left out), with its kind, its description, the list that defines it, and its schema versions — the organization's own and the official organization's, each labelled by `source`. `fallback` is one of them; there is no `default` row. A type with no versions resolves down the ladder at extraction time.\n","responses":{"200":{"description":"One row per document type, by name; `fallback` first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocumentTypeListing"}}}}}}}},"/doc-type-extraction-schemas":{"post":{"operationId":"createDocTypeExtractionSchema","tags":["doc-type-extraction-schemas"],"summary":"Create a v1 draft extraction schema (and the document type, if new)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocTypeExtractionSchemaInput"}}}},"responses":{"200":{"description":"The name already means an existing document; that one is returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocTypeExtractionSchemaRef"}}}},"201":{"description":"Created draft reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocTypeExtractionSchemaRef"}}}},"400":{"description":"The name yields no usable id, or the id is reserved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/doc-type-extraction-schemas/{docType}/{version}":{"get":{"operationId":"getDocTypeExtractionSchema","tags":["doc-type-extraction-schemas"],"summary":"Full extraction schema JSON for one version","parameters":[{"$ref":"#/components/parameters/DocType"},{"$ref":"#/components/parameters/Version"}],"responses":{"200":{"description":"The schema","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocTypeExtractionSchema"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}},"put":{"operationId":"saveDocTypeExtractionSchema","tags":["doc-type-extraction-schemas"],"summary":"Save a draft (a draft is the only writable status)","parameters":[{"$ref":"#/components/parameters/DocType"},{"$ref":"#/components/parameters/Version"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocTypeExtractionSchema"}}}},"responses":{"200":{"description":"Saved schema","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocTypeExtractionSchema"}}}},"400":{"description":"Contract validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"Version is active or superseded, and immutable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/doc-type-extraction-schemas/{docType}/{version}/new-version":{"post":{"operationId":"createDocTypeExtractionSchemaVersion","description":"On the caller's own schema: vN → v(N+1) draft. On the official organization's schema (read-only for everyone else): the caller's own version 1 draft, copied from it — 409 when the caller already has versions for that type.\n","tags":["doc-type-extraction-schemas"],"summary":"Copy vN to v(N+1) as a draft; vN keeps its status","parameters":[{"$ref":"#/components/parameters/DocType"},{"$ref":"#/components/parameters/Version"}],"responses":{"201":{"description":"The new draft reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocTypeExtractionSchemaRef"}}}},"409":{"description":"A newer version already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/doc-type-extraction-schemas/{docType}/{version}/rename-as-new-version":{"post":{"operationId":"renameDocTypeExtractionSchemaAsNewVersion","tags":["doc-type-extraction-schemas"],"summary":"Copy vN to v(N+1) as a draft under a new name; vN keeps its own","description":"How an active schema is renamed. The version itself never changes name, so the new one arrives on the version after it. Branch and name are a single write: a failure leaves no half-renamed copy behind.\n","parameters":[{"$ref":"#/components/parameters/DocType"},{"$ref":"#/components/parameters/Version"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameInput"}}}},"responses":{"201":{"description":"The new draft reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocTypeExtractionSchemaRef"}}}},"409":{"description":"A newer version already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/doc-type-extraction-schemas/{docType}/{version}/activate":{"post":{"operationId":"activateDocTypeExtractionSchema","tags":["doc-type-extraction-schemas"],"summary":"Activate a draft; it supersedes the active version it replaces, and extraction reads it","parameters":[{"$ref":"#/components/parameters/DocType"},{"$ref":"#/components/parameters/Version"}],"responses":{"200":{"description":"The activated schema","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocTypeExtractionSchema"}}}},"409":{"description":"Not a draft, or the schema is not complete enough to activate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/workflow-tools":{"get":{"operationId":"listWorkflowTools","tags":["workflow-library"],"summary":"The step kinds a workflow may contain, and what each exposes as authorable","responses":{"200":{"description":"Preview and Parse (the spine) followed by every registered tool","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkflowToolOption"}}}}}}}},"/workflows":{"get":{"operationId":"listWorkflows","tags":["workflow-library"],"summary":"Every workflow in the library, with its versions","responses":{"200":{"description":"One row per workflow, newest version first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkflowListing"}}}}}}},"post":{"operationId":"createWorkflow","tags":["workflow-library"],"summary":"Create a v1 draft workflow","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkflowInput"}}}},"responses":{"201":{"description":"Created draft reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkflowRef"}}}},"400":{"description":"The name yields no usable id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"A workflow with that id already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/workflows/{workflowId}/runs":{"post":{"operationId":"startWorkflowRun","tags":["runs"],"summary":"The door — run a workflow on a packet in one call","description":"Creates the application (or takes `applicationId`), receives the PDFs as unsolicited source files, and takes the gate with this workflow — the active version unless `version` pins one (a draft is 409). The call is the one consent workflow mode needs: the caller is recorded as `decidedBy` and `authenticatedAs`. Red flags at pre-flight are not bypassed on the caller's behalf: 409 with `flags` and `applicationId`, and the case waits at the gate for a person. Then poll GET /applications/{id}: `run.state` reaches `report` when the workflow has filed what it could; `run.lastRunError` says it failed.\n","parameters":[{"name":"workflowId","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"query","required":false,"schema":{"type":"integer","minimum":1},"description":"Pin a workflow version; otherwise the active one runs."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/WorkflowRunInput"}}}},"responses":{"201":{"description":"The application with the run started (run.state previewing or running)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"400":{"description":"No files, missing applicant or expectedDocList, or a bad id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GateRefused"}}}},"403":{"description":"The organization is an instant account with no email yet (`emailRequired` is true; the case exists and waits at the gate)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GateRefused"}}}},"404":{"description":"Unknown workflow, application or expected document list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GateRefused"}}}},"409":{"description":"Red flags at pre-flight (body carries `flags` and `applicationId`; the case is created and its files received), no active version, a draft pinned, or a run already in flight\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GateRefused"}}}},"502":{"description":"Analyzer unreachable — the case stays gated with its files","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GateRefused"}}}}}}},"/workflows/{workflowId}/{version}":{"get":{"operationId":"getWorkflow","tags":["workflow-library"],"summary":"Full workflow JSON for one version","parameters":[{"$ref":"#/components/parameters/WorkflowId"},{"$ref":"#/components/parameters/Version"}],"responses":{"200":{"description":"The workflow","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workflow"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}},"put":{"operationId":"saveWorkflow","tags":["workflow-library"],"summary":"Save a draft (active versions are immutable)","parameters":[{"$ref":"#/components/parameters/WorkflowId"},{"$ref":"#/components/parameters/Version"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workflow"}}}},"responses":{"200":{"description":"Saved workflow","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workflow"}}}},"400":{"description":"Contract validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"Version is active and immutable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/workflows/{workflowId}/{version}/new-version":{"post":{"operationId":"createWorkflowVersion","tags":["workflow-library"],"summary":"Copy vN to v(N+1) as a draft; vN keeps its status","parameters":[{"$ref":"#/components/parameters/WorkflowId"},{"$ref":"#/components/parameters/Version"}],"responses":{"201":{"description":"The new draft reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkflowRef"}}}},"409":{"description":"A newer version already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/workflows/{workflowId}/{version}/activate":{"post":{"operationId":"activateWorkflow","tags":["workflow-library"],"summary":"Draft to active (one-way); the gate runs the highest active version","parameters":[{"$ref":"#/components/parameters/WorkflowId"},{"$ref":"#/components/parameters/Version"}],"responses":{"200":{"description":"The activated workflow","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workflow"}}}},"409":{"description":"Already active, or the workflow is not complete enough to activate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/workflows/{workflowId}/set-default":{"post":{"operationId":"setDefaultWorkflow","tags":["workflow-library"],"summary":"Make this the workflow the pre-flight gate pre-selects — exactly one, set atomically","parameters":[{"$ref":"#/components/parameters/WorkflowId"}],"responses":{"200":{"description":"The library after the change","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkflowListing"}}}}},"409":{"description":"The workflow has no active version, so it cannot be the default","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/intel-templates":{"get":{"operationId":"listIntelTemplates","tags":["intel-library"],"summary":"Every Intel template in the library, with its versions","responses":{"200":{"description":"One row per template, newest version first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IntelTemplateListing"}}}}}}},"post":{"operationId":"createIntelTemplate","tags":["intel-library"],"summary":"Create a v1 draft template with no questions yet","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntelTemplateInput"}}}},"responses":{"201":{"description":"Created draft reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntelTemplateRef"}}}},"400":{"description":"The name yields no usable id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"A template with that id already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/intel-templates/{templateId}/{version}":{"get":{"operationId":"getIntelTemplate","tags":["intel-library"],"summary":"One version of a template","parameters":[{"$ref":"#/components/parameters/TemplateId"},{"$ref":"#/components/parameters/Version"}],"responses":{"200":{"description":"The template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntelTemplate"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}},"put":{"operationId":"saveIntelTemplate","tags":["intel-library"],"summary":"Save a draft (active and superseded versions are immutable)","parameters":[{"$ref":"#/components/parameters/TemplateId"},{"$ref":"#/components/parameters/Version"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntelTemplate"}}}},"responses":{"200":{"description":"Saved template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntelTemplate"}}}},"400":{"description":"Contract validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"The version is not a draft","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/intel-templates/{templateId}/{version}/new-version":{"post":{"operationId":"createIntelTemplateVersion","tags":["intel-library"],"summary":"Copy an active vN to v(N+1) as a draft; vN keeps its status","parameters":[{"$ref":"#/components/parameters/TemplateId"},{"$ref":"#/components/parameters/Version"}],"responses":{"201":{"description":"The new draft reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntelTemplateRef"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"Not active, or a newer version already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/intel-templates/{templateId}/{version}/activate":{"post":{"operationId":"activateIntelTemplate","tags":["intel-library"],"summary":"Draft to active (one-way); supersedes the version it replaces","parameters":[{"$ref":"#/components/parameters/TemplateId"},{"$ref":"#/components/parameters/Version"}],"responses":{"200":{"description":"The activated template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntelTemplate"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"Not a draft, or not complete enough to activate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/intel-briefs":{"get":{"operationId":"listIntelBriefs","tags":["intel-briefs"],"summary":"The case's briefs, newest first","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"responses":{"200":{"description":"One row per brief","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IntelBriefListing"}}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}},"post":{"operationId":"startIntelBrief","tags":["intel-briefs"],"summary":"Answer a template's active version over the case; returns at once with status running","description":"One request per question to the research model, over every page of every active file. The brief fills in as questions finish; poll GET .../intel-briefs/{briefId}. A section that fails is shown failed inside a ready brief; each records its model and token spend.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartIntelBriefInput"}}}},"responses":{"202":{"description":"The brief, running","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntelBrief"}}}},"400":{"description":"Invalid body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application or template not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"The template has no active version, the case has no parsed pages, or this is a sample case","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"429":{"description":"Too many briefs for a workspace with no email yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/intel-briefs/{briefId}":{"get":{"operationId":"getIntelBrief","tags":["intel-briefs"],"summary":"One brief, with its sections and citations","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"$ref":"#/components/parameters/BriefId"}],"responses":{"200":{"description":"The brief","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntelBrief"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/saved-sections":{"get":{"operationId":"listSavedSections","tags":["saved-sections"],"summary":"Reusable section fragments for the builder palette","responses":{"200":{"description":"Fragments","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SavedSection"}}}}}}},"post":{"operationId":"createSavedSection","tags":["saved-sections"],"summary":"Save a section as a reusable fragment (a copy, never a link)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSectionInput"}}}},"responses":{"201":{"description":"Saved fragment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSection"}}}}}}},"/document-bundles":{"get":{"operationId":"listDocumentBundles","tags":["bundle-catalogue"],"summary":"Catalogue bundles available to every expected document list","responses":{"200":{"description":"Bundles, by name","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocumentBundle"}}}}}}},"post":{"operationId":"createDocumentBundle","tags":["bundle-catalogue"],"summary":"Create a catalogue bundle from existing document types","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentBundleInput"}}}},"responses":{"201":{"description":"Created bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentBundle"}}}},"400":{"description":"Empty name, no members, or a repeated member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"A bundle with that name already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/document-bundles/{bundleId}":{"parameters":[{"in":"path","name":"bundleId","required":true,"schema":{"type":"string"}}],"put":{"operationId":"updateDocumentBundle","tags":["bundle-catalogue"],"summary":"Rename a catalogue bundle or change which documents it contains","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentBundleInput"}}}},"responses":{"200":{"description":"Updated bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentBundle"}}}},"400":{"description":"Empty name, no members, or a repeated member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"No such bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}},"delete":{"operationId":"deleteDocumentBundle","tags":["bundle-catalogue"],"summary":"Delete a catalogue bundle. Expected document lists that already inserted it keep their subsection untouched — the insert was a copy, not a link.\n","responses":{"204":{"description":"Deleted"},"404":{"description":"No such bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/access-matrix":{"get":{"operationId":"getAccessMatrix","tags":["access"],"summary":"The access matrix as the server enforces it, and the caller's role","responses":{"200":{"description":"Rights, roles with their rights, and `you`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessMatrix"}}}}}}},"/api-keys":{"get":{"operationId":"listApiKeys","tags":["keys"],"summary":"Managers only — the organization's API keys, active and revoked, newest first","responses":{"200":{"description":"Keys. The secret is never returned here.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}},"403":{"description":"Not a Manager, or the caller is itself an API key"}}},"post":{"operationId":"createApiKey","tags":["keys"],"summary":"Managers only — mint a key; the secret is returned once","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyInput"}}}},"responses":{"201":{"description":"The key with its secret — the only time the secret is returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedApiKey"}}}},"403":{"description":"Not a Manager, or the caller is itself an API key"}}}},"/api-keys/{id}":{"delete":{"operationId":"revokeApiKey","tags":["keys"],"summary":"Managers only — revoke a key; the row stays for the record","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Revoked (or already revoked)"},"404":{"description":"No such key in this organization"}}}},"/webhooks":{"get":{"operationId":"listWebhooks","tags":["webhooks"],"summary":"Managers only — the organization's endpoints, newest first, each with its latest delivery","responses":{"200":{"description":"Endpoints. The secret is never returned here.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}}}},"403":{"description":"Not a Manager, or the caller is an API key"}}},"post":{"operationId":"createWebhook","tags":["webhooks"],"summary":"Managers only — add an endpoint; the signing secret is returned once","description":"Deliveries are ledger rows: `{ id, type, at, application, data: { actor, target, detail } }` where `type` is the ledger action name. Each POST carries Standard Webhooks headers — `webhook-id`, `webhook-timestamp`, and `webhook-signature` = `v1,` + base64 HMAC-SHA256 with the `whsec_` secret over `id.timestamp.body`. Answer 2xx within 15 s; otherwise the delivery retries 14 times over about 13 hours (30 s → 2 h); a 410 deactivates the endpoint. `events` empty means every event. Nothing from before the endpoint existed is replayed.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookInput"}}}},"responses":{"201":{"description":"The endpoint with its secret — the only time the secret is returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedWebhookEndpoint"}}}},"400":{"description":"Not an http(s) URL, or an unknown event name","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"403":{"description":"Not a Manager, or the caller is an API key"}}}},"/webhooks/{id}":{"delete":{"operationId":"deleteWebhook","tags":["webhooks"],"summary":"Managers only — remove an endpoint and its delivery history","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Removed"},"404":{"description":"No such endpoint in this organization"}}}},"/webhooks/{id}/test":{"post":{"operationId":"testWebhook","tags":["webhooks"],"summary":"Managers only — send one signed `webhook.test` delivery now and report the receiver's answer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"What the receiver answered (a 410 deactivates the endpoint, as in the outbox)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestResult"}}}},"404":{"description":"No such endpoint in this organization"}}}},"/users":{"get":{"operationId":"listUsers","tags":["users"],"summary":"The signed-in user's organization — its people","responses":{"200":{"description":"Users","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}},"post":{"operationId":"createUser","tags":["users"],"summary":"Managers only — add a colleague to the organization","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserInput"}}}},"responses":{"201":{"description":"The new user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"403":{"description":"Not a Manager"},"409":{"description":"That email already has an account"}}}},"/login":{"post":{"operationId":"login","security":[],"tags":["users"],"summary":"Verify credentials and set the signed session cookie","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginInput"}}}},"responses":{"200":{"description":"Authenticated user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Wrong username or password"}}}},"/signup":{"post":{"operationId":"signup","security":[],"tags":["users"],"summary":"Create an organization with its first Manager (public); sets the session cookie","description":"With `orgName`, `email` and `password` this is the sign-up form. With no body it is an instant account: the organization and its Manager get a generated three-word name, there is no password, and the organization is kept for 30 days unless an email is added (`PATCH /users/{id}` with `email`). `email` and `password` come together or not at all.","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupInput"}}}},"responses":{"201":{"description":"The new Manager, signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"An email without a password (or the reverse), or a field that is too short"},"409":{"description":"That email already has an account"},"429":{"description":"Too many sign-ups from here — try later"}}}},"/contact":{"post":{"operationId":"contact","security":[],"tags":["contact"],"summary":"The marketing site's contact form (public; honeypot field `company`; throttled per IP)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactInput"}}}},"responses":{"200":{"description":"Sent — or silently accepted when the honeypot was filled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactAck"}}}},"400":{"description":"Missing name, invalid email, or nothing picked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"429":{"description":"Throttled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/logout":{"post":{"operationId":"logout","tags":["users"],"summary":"Clear the session cookie","responses":{"204":{"description":"Signed out"}}}},"/me":{"get":{"operationId":"getMe","tags":["users"],"summary":"The signed-in user, from the session cookie","responses":{"200":{"description":"The signed-in user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}}}},"/users/{id}":{"patch":{"operationId":"updateUser","tags":["users"],"summary":"Managers only — edit a person (name, email, role, active, temporary password)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserInput"}}}},"responses":{"200":{"description":"The updated user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"403":{"description":"Not a Manager"},"404":{"description":"No such user in this organization"},"409":{"description":"Refused — you cannot deactivate or demote yourself, or the last active Manager; or that email already has an account"}}}},"/organization":{"get":{"operationId":"getOrganization","tags":["users"],"summary":"The signed-in person's organization — its name, its marked document catalogue and whether it is the official one","responses":{"200":{"description":"The organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}}}},"patch":{"operationId":"renameOrganization","tags":["users"],"summary":"Managers only — rename the organization (its id never changes)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameOrganizationInput"}}}},"responses":{"200":{"description":"The organization under its new name","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"403":{"description":"Not a Manager"}}}},"/organization/marked-doc-type-catalog":{"put":{"operationId":"setMarkedDocTypeCatalog","tags":["users"],"summary":"Managers only — which official document catalogue the organization classifies against","description":"The id of a `docTypeCatalogOfficial` list of the official organization with an active version (`generic`, `finance`, …). Absent on the organization = `generic`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkedDocTypeCatalogInput"}}}},"responses":{"200":{"description":"The organization, with the catalogue marked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"403":{"description":"Not a Manager"},"409":{"description":"Not an official document catalogue with an active version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/password":{"post":{"operationId":"changePassword","tags":["users"],"summary":"Change your own password (ends your other sessions; this one is re-issued)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordInput"}}}},"responses":{"204":{"description":"Changed"},"403":{"description":"The current password is wrong (the session itself is fine)"}}}},"/password/forgot":{"post":{"operationId":"forgotPassword","security":[],"tags":["users"],"summary":"Email a one-hour, single-use reset link (public; always 200)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordInput"}}}},"responses":{"200":{"description":"Accepted, whether or not the email is known"}}}},"/password/reset":{"post":{"operationId":"resetPassword","security":[],"tags":["users"],"summary":"Set a new password from a reset link (public)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordInput"}}}},"responses":{"204":{"description":"Password set; every session for that user has ended"},"400":{"description":"The link is no longer valid"}}}},"/guest":{"post":{"operationId":"openGuestSandbox","tags":["guest"],"summary":"A guest key for trying the API — a sandbox with the sample case, no account, no password (public)","description":"One call, no body. Makes a sandbox organization holding a private copy of the sample case and answers with a key for it. The key acts like any organization key — read, search, upload, run a workflow, ask — within three limits: 10 requests a minute, 24 hours, and 50 pages of uploads (an organization without an email holds 50 pages). After 24 hours the key is refused and the sandbox is deleted with everything in it. Guarded per address like sign-up.\n","responses":{"201":{"description":"The sandbox and its key. The key is shown once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestAccess"}}}},"429":{"description":"Too many guest sandboxes from this address — try later, or make an account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"503":{"description":"No sample case is marked yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications":{"get":{"operationId":"listApplications","tags":["intake"],"summary":"All applications with progress summary","responses":{"200":{"description":"Applications","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationSummary"}}}}}}},"post":{"operationId":"createApplication","tags":["intake"],"summary":"Start an application pinned to an active expected document list version","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationInput"}}}},"responses":{"201":{"description":"Created application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"409":{"description":"Version is not active","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}":{"get":{"operationId":"getApplication","tags":["intake"],"summary":"Application with its intake state and pinned expected document list","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"responses":{"200":{"description":"Application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}},"patch":{"operationId":"updateApplication","tags":["intake"],"summary":"Update portal-owned application fields","description":"projectedClosingDate is a first-class portal-owned field (analyzer spec §8) — the hard-expiry clocks are judged against it. Null clears it.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationUpdate"}}}},"responses":{"200":{"description":"Updated application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/expected-doc-list-version":{"post":{"operationId":"upgradeExpectedDocListVersion","tags":["intake"],"summary":"Re-pin the application to a newer active expected document list version","description":"Additive-only upgrade of the application's pinned expected document list copy. The target must be a NEWER, ACTIVE version of the same expected document list, and every expected document on the current pin must still exist in the target — files, approvals and analysis mappings key on those expected document ids, so downgrades, re-pins and expected-document-removing moves are refused (409). Each successful re-pin appends who/when/vN→vN to expectedDocListHistory for the audit trail.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpectedDocListUpgradeInput"}}}},"responses":{"200":{"description":"Application re-pinned to the target version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"400":{"description":"Malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application or expected document list version not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"Downgrade/re-pin, inactive target, or target removes expected documents in use","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/fields/{expectedDocId}":{"put":{"operationId":"saveFieldValues","tags":["intake"],"summary":"Save the values of a field-group expected document","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"$ref":"#/components/parameters/ExpectedDocId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldValues"}}}},"responses":{"200":{"description":"Updated application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}}}}},"/applications/{applicationId}/pages-pdf":{"get":{"operationId":"getPagesPdf","tags":["files"],"summary":"Any pages of a case as one PDF, in the order given","description":"Cuts the named pages from the case's files into one PDF, in the order the spans are given — one page, or a document as the review room shows it, drafts included. Each `span` is `<fileId>:p<first>-<last>`, the shape of a document address's segments; repeat it for several. The bytes come back as an attachment.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"name":"span","in":"query","required":true,"style":"form","explode":true,"schema":{"type":"array","minItems":1,"items":{"type":"string"}}}],"responses":{"200":{"description":"The pages, as one PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"A span that is malformed, names no file of the case, or runs past its pages","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/files":{"post":{"operationId":"receiveFiles","tags":["files"],"summary":"Unified file receive — every file that enters the system lands here as an immutable SourceFile","description":"Multi-file multipart receive (field name \"files\"). Optional expectedDocId/setId query params declare solicited intent (origin becomes \"solicited\"); without them the drop is \"unsolicited\". Whole-drop validation — one bad file rejects the drop. Bytes are stored id-addressed and immutable; renames never touch storage.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"name":"expectedDocId","in":"query","required":false,"schema":{"type":"string"},"description":"Solicited intent — must be an expected document on the pinned expected document list."},{"name":"setId","in":"query","required":false,"schema":{"type":"string"},"description":"Solicited intent — one of the expected document's sets (400 if unknown)."},{"name":"derivedFromFileId","in":"query","required":false,"schema":{"type":"string"},"description":"A region cut from a page of this application (all three derivedFrom parameters, or none): the source file. The one uploaded image becomes a derived file (kind \"derived\", derivation op \"crop\").\n"},{"name":"derivedFromPage","in":"query","required":false,"schema":{"type":"integer","minimum":1},"description":"The source page, in-file."},{"name":"derivedFromBox","in":"query","required":false,"schema":{"type":"string","pattern":"^[0-9.]+(,[0-9.]+){3}$"},"description":"The region as \"x,y,w,h\", fractions of the page, top-left origin."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"files":{"type":"array","items":{"type":"string","format":"binary"}}}}}}},"responses":{"201":{"description":"The received SourceFile records (whole application returned for state sync)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiveFilesResponse"}}}},"400":{"description":"Validation failure — whole drop rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"403":{"description":"The caller's role may not add to that section of the expected document list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/files/{fileId}":{"patch":{"operationId":"updateSourceFile","tags":["files"],"summary":"Metadata-only update — rename (bytes untouched, recorded in the ledger)","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"name":"fileId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourceFileBody"}}}},"responses":{"200":{"description":"Updated application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"404":{"description":"No such file"}}}},"/applications/{applicationId}/expected-docs/{expectedDocId}/sets":{"post":{"operationId":"addSet","tags":["sets"],"summary":"Add a set to a set expected document on this application","description":"Descriptor keys must match the expected document's descriptorFields exactly; values non-empty; duplicate descriptor value-sets rejected (409). Label is built server-side; the id is minted once.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"$ref":"#/components/parameters/ExpectedDocId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["descriptor"],"properties":{"descriptor":{"type":"object","additionalProperties":{"type":"string"}}}}}}},"responses":{"201":{"description":"The created set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationSet"}}}},"400":{"description":"Expected document is not a set expected document, or descriptor keys/values invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"A set with the same descriptor already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/expected-docs/{expectedDocId}/sets/{setId}":{"delete":{"operationId":"deleteSet","tags":["sets"],"summary":"Remove a set (refused while files are tagged to it)","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"$ref":"#/components/parameters/ExpectedDocId"},{"name":"setId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"409":{"description":"Files still tagged to this set — untag or delete them first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/approved-docs":{"get":{"operationId":"listApprovedDocs","tags":["approved-docs"],"summary":"The approved-document registry for an application (newest first, superseded rows included)","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"responses":{"200":{"description":"Registry rows","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApprovedDocument"}}}}}}}},"/applications/{applicationId}/approved-docs/{expectedDocId}/materialize":{"post":{"operationId":"materializeApprovedDoc","tags":["approved-docs"],"summary":"Retry approval materialization for a filed expected document","description":"Re-runs materialization from the expected document's LIVE approvals — the decision already stands, only its registry copy is missing. Takes no body: whatever is currently filed against the expected document is what gets materialized, and a set expected document retries every set approval still missing its row. Success clears the expected document's materializationErrors entry; failure updates it — loudly, with the reason.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"$ref":"#/components/parameters/ExpectedDocId"}],"responses":{"201":{"description":"The materialized registry row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovedDocument"}}}},"409":{"description":"Nothing to materialize — no live approval on this expected document is missing its registry row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"502":{"description":"Materialization failed (worker or storage) — reason included, recorded on the application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/split-resolutions":{"post":{"operationId":"recordSplitResolution","tags":["approved-docs"],"summary":"Record (or change) the human decision on an analyzer split recommendation","description":"A split recommendation (the analyzer read more than one document inside one managed document) must be resolved — split or dismissed — before that document can be approved. Upsert keyed by the document's own span; latest decision wins, so a dismissal can be revisited. Portal-owned, additive. The split itself is virtual: approving one divides the group in the room, it does not chop the raw file.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SplitResolutionInput"}}}},"responses":{"200":{"description":"Updated application (splitResolutions upserted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"400":{"description":"Invalid body or range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/merge-resolutions":{"post":{"operationId":"recordMergeResolution","tags":["approved-docs"],"summary":"Record (or change) the human decision on an analyzer merge recommendation","description":"A merge recommendation (two run groups filed under the same expected document) must be resolved — merged or dismissed — before either group can be approved. Upsert keyed by run + the two page ranges; latest decision wins, so a dismissed recommendation can be revisited. Portal-owned, additive.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeResolutionInput"}}}},"responses":{"200":{"description":"Updated application (mergeResolutions upserted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"400":{"description":"Invalid body or ranges","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/document-approvals":{"post":{"operationId":"recordDocumentApproval","tags":["approved-docs"],"summary":"Record a per-document approval from the filmstrip flow","description":"The document-level confirmation of the page-by-page pre-step — the FILING decision (expected document + set). approved / approved_incomplete run the same materialization seam as expected document accepts; approved_incomplete additionally marks a new version as requested. rejected records the decision only. Materialization failure keeps the approval (recorded on materializationErrors, retryable) — the decision stands.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentApprovalInput"}}}},"responses":{"200":{"description":"Updated application (documentApprovals prepended; materializationErrors reflects any failure)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"400":{"description":"Invalid body, unknown expected document, or pages outside the run","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"Set required for a set expected document, or unknown set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/approved-docs/{approvedDocId}/file":{"get":{"operationId":"getApprovedDocFile","tags":["approved-docs"],"summary":"Download an approved document's bytes (pdf) or provenance sidecar (md)","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"name":"approvedDocId","in":"path","required":true,"schema":{"type":"string"}},{"name":"kind","in":"query","required":false,"schema":{"type":"string","enum":["pdf","md"],"default":"pdf"}}],"responses":{"200":{"description":"File stream","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/models/options":{"get":{"operationId":"listModelOptions","tags":["models"],"summary":"Per-stage model options for the run plan","description":"The analyzer worker's model registry, proxied verbatim — per-stage options (parse / text / judge) with validation status and live availability (missing keys make an option unavailable, never a silent fallback). The dropdowns on the gate card render exactly this; the chosen ids travel with the gate decision.\n","responses":{"200":{"description":"Stage-by-stage options","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelOptionsResponse"}}}},"502":{"description":"Analyzer worker unreachable — options unknown, no guessing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/agent/chat":{"post":{"operationId":"agentChat","tags":["agent-stream"],"summary":"The case assistant — one turn, streamed as AI SDK UI message events","description":"Streams the assistant's turn as server-sent events in the AI SDK UI message protocol. The web client drives it with the AI SDK transport, not the generated client. `mode` is read-only unless the panel's Full control toggle is on; write tools then need the human's approval card.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentChatInput"}}}},"responses":{"200":{"description":"The assistant's turn, streamed","content":{"text/event-stream":{"schema":{"type":"string"}}}},"400":{"description":"Invalid messages","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application not found"}}}},"/applications/{applicationId}/citations/resolve":{"get":{"operationId":"resolveCitation","tags":["agent"],"summary":"Locate a quoted citation on a page — fractional boxes for the highlight","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"name":"fileId","in":"query","required":true,"schema":{"type":"string"}},{"name":"page","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"quote","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"matched=false when the quote cannot be located; boxes are fractions of the page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolvedCitation"}}}},"404":{"description":"No elements projection for that file/page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/events":{"get":{"operationId":"listApplicationEvents","tags":["ledger"],"summary":"Append-only event ledger for one application","description":"Newest first. Every state change (file arrivals, gate decisions, approvals, run ingests, expected document list repins…) writes one row in the same transaction as the change. Rows are never updated or deleted.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"responses":{"200":{"description":"Events, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListApplicationEventsResponse"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/documents":{"get":{"operationId":"listApplicationDocuments","tags":["documents"],"summary":"Every document an application holds","description":"The application's documents, ordered by first page. This is the answer to \"what does this application hold\" — NOT \"what did the last run produce\". A run is a reading of some files at a moment; documents outlive it, because approvals are append-only and a document approved under an earlier run is still held today. After a split, documents outnumber the files they came from.\n\nA document has no id. Its identity is its `address`: the canonical rendering of the page spans it covers. Split and merge therefore need no id bookkeeping — a split is new addresses, a merge is one. Address by `spans`; `pages` is a derived convenience for display and ordering.\n\nDocuments whose spans address a file the application no longer holds are omitted — they are, by that fact, no longer held.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"name":"scope","in":"query","required":false,"description":"`application` (default) is every document held, including those approved under earlier runs. `run` narrows to documents the latest run produced.\n","schema":{"type":"string","enum":["application","run"],"default":"application"}}],"responses":{"200":{"description":"Documents, ordered by first page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListApplicationDocumentsResponse"}}}},"400":{"description":"Invalid application id or scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/text":{"get":{"operationId":"getApplicationText","tags":["documents"],"summary":"The case's text in one response","description":"Every current document's saved copy, verbatim, in page order, one after another: each begins with its own front matter (`---`) naming the document, what it is filed under and the pages it covers, and a `<!-- <fileId> p.<n> -->` marker precedes each page's text. Fetch it once and search locally. A document without a saved copy (read before copies existed) is a short front-matter stub saying so.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"responses":{"200":{"description":"The documents' copies, in page order","content":{"text/markdown":{"schema":{"type":"string"}}}},"400":{"description":"Invalid application id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/analysis":{"get":{"operationId":"getAnalysis","tags":["analysis"],"summary":"Analyzer sidecar for an application","description":"Returns an empty shell (latestRunId null, runs []) when no run has been ingested yet.","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"responses":{"200":{"description":"Sidecar","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisSidecar"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/run/gate":{"post":{"operationId":"decideRunGate","tags":["runs"],"summary":"The staff gate decision — kicks the analyzer on the ACTIVE FILE SET","description":"File-native runs: the gate covers a set of files, not a blob. Valid only while no run is in flight (409 otherwise) and at least one active PDF SourceFile exists (400 otherwise). decision=confirmed (\"Process\") or bypassed (\"Skip checks\"). The server freezes the input set (fileId + sha256 + pages), mints a requestId and kicks the analyzer worker. mode=preview (default) flips run.state to previewing and runs the cheap inventory pass; the paid tools then start from the inventory step (/run/parse). mode=parse flips run.state to running (kind=parse, prior=gated) and parses every gated file whole, skipping preview — the click is the consent, and `tools` chains the post-parse sequence into that same kick so the whole run happens on one click. Kick failure reverts to gated with lastRunError set — never a silent hang.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunGateInput"}}}},"responses":{"200":{"description":"Updated application (run.state=previewing)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"400":{"description":"Malformed decision, or a file has no page count from pre-flight","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"403":{"description":"The organization is an instant account with no email yet — a Manager adds one, then runs start (`emailRequired` is true)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GateRefused"}}}},"404":{"description":"Application or workflow not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"A run is already in flight, nothing new to analyze, a draft workflow, or the decision does not match the red flags (confirmed with flags standing → the body carries `flags`; bypassed with none)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GateRefused"}}}},"502":{"description":"Analyzer could not be reached — reverted to gated, decision not recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/run/cancel":{"post":{"operationId":"cancelRun","tags":["runs"],"summary":"Stop the paid run in flight","description":"Stops a parse, classify, judge or extract run that is still running and puts the application back where that run started (run.prior), exactly as a failure does: whatever already landed stands, and pages the parse had already read stay stored. The stop is recorded as run.cancelled, and the analyzer is told to stop spending. The portal stops the run first and tells the worker second, so a worker that has gone silent cannot keep an application stuck — a late result from it is kept as a record but flips no state and files nothing.\nA preview cannot be stopped midway: its rows are settled by the preview itself, and it ends on its own timeout. The portal also stops a paid run by itself when the analyzer has sent nothing, not even its periodic heartbeat, for three minutes — that stop is recorded as run.failed.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"responses":{"200":{"description":"Updated application (run.state back at run.prior)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"No paid run is in flight (nothing running, or a preview)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/run/parse":{"post":{"operationId":"startScopedParse","tags":["runs"],"summary":"Start the paid full parse for selected inventory entries","description":"The inventory step's \"Parse\" — spends real money on exactly the selected spans, and AUTO-CHAINS every flow-enabled tool over them in the same kick (tool-runs §2c: uniform scrutiny — one kick, one run, one union). Valid only when the latest run carries an inventory and no run is in flight (409 otherwise). The request's plan/settings freeze into flow.parse and become the defaults for later delta parses. The inventory step waits for an explicit click: a human-driven preview run does not advance itself. A workflow run has no inventory step — it is authorized once at the gate and runs through.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartScopedParseInput"}}}},"responses":{"200":{"description":"Updated application (run.state=running, flow tools chained)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"400":{"description":"Unknown entry keys or no inventory on the latest run","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"A run is already in flight","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"502":{"description":"Analyzer could not be reached — reverted to inventory, nothing started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/run/tool":{"post":{"operationId":"startToolRun","tags":["runs"],"summary":"Run a post-parse workflow tool — add-to-flow backfill, extraction, or targeted re-run","description":"The header's \"+ Tool\" and per-tool re-run (tool-runs plan §2c).\njudge — without targets: one consented backfill over every parsed-but-unjudged document, and the tool (with its frozen plan) is recorded into the application flow, so every later scoped parse auto-chains it (uniform scrutiny). With targets (each must equal a parsed document's span): a re-judge of exactly those documents — latest overlay wins.\nextract — schema-directed extraction over already-parsed documents. Each document is read against the type it is CURRENTLY filed under (a human's manual placement wins over the analyzer's recommendation), with the Default schema as the fallback when that type has no authored one, so every document in scope costs a text call. Deliberately NOT a flow tool: it never auto-chains, because extraction reads each document against the type it is FILED under, so filing has to have happened first. (In workflow mode filing lands automatically when the run does, so this becomes available immediately — but it stays its own run and its own spend.) Documents a human archived are skipped.\nValid only with a landed parse blob, from the inventory or report state, with no run in flight (409 otherwise). Money moves on THIS click.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartToolRunInput"}}}},"responses":{"200":{"description":"Updated application (run.state=running, kind=judge)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"400":{"description":"A target does not match a parsed document's span, or its file is archived","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"A run is in flight, no parsed documents exist, or nothing is unjudged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"502":{"description":"Analyzer could not be reached — reverted, nothing started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/flow/{tool}":{"delete":{"operationId":"removeFlowTool","tags":["runs"],"summary":"Remove a tool from the application flow — future parses stop chaining it","description":"Tool-runs plan §2c: removal only edits the flow. Existing overlays stay on the documents (audit trail — they describe runs that DID happen); nothing is re-run, nothing is deleted. 404 when the tool is not in the flow; 409 while a run is in flight.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"name":"tool","in":"path","required":true,"schema":{"type":"string","enum":["judge"]}}],"responses":{"200":{"description":"Updated application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"404":{"description":"Application not found, or the tool is not in the flow","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"409":{"description":"A run is in flight","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/inventory/{entryKey}":{"patch":{"operationId":"renameInventoryEntry","tags":["runs"],"summary":"Rename an inventory entry — nameSource becomes user, which wins permanently","description":"Human rename of a preview entry's provisional name. Recorded on the LATEST run's inventory with nameSource=user; re-previews and the full parse never overwrite a user-edited name (merge is span-keyed).\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"name":"entryKey","in":"path","required":true,"schema":{"type":"string"},"description":"span-derived entry identity, fileId:pFIRST-LAST"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameInventoryEntryInput"}}}},"responses":{"200":{"description":"Updated analysis sidecar (latest run carries the rename)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisSidecar"}}}},"400":{"description":"Invalid entry key or name","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application, run inventory, or entry not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/files/{fileId}/pages/{page}":{"get":{"operationId":"getFilePageImage","tags":["analysis"],"summary":"Full-page PNG render for a source file (file-keyed, run-independent)","description":"Served from object storage, collocated with the file's bytes (files/<fileId>/pages|thumbnails). Pages are addressed (fileId, page) — 1-based within the file. Files are immutable, so a render never changes and survives across (delta) runs. size=thumb returns the 320px-wide filmstrip thumbnail; size=full returns the render at analyzer DPI. Responses cache aggressively.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"name":"fileId","in":"path","required":true,"schema":{"type":"string"}},{"name":"page","in":"path","required":true,"schema":{"type":"integer"}},{"name":"size","in":"query","required":false,"schema":{"type":"string","enum":["full","thumb"]}}],"responses":{"200":{"description":"PNG render of the page","content":{"image/png":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Invalid application id, run id or page number","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"No render for this page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"502":{"description":"Object storage unreachable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/files/{fileId}/pages/{page}/elements":{"get":{"operationId":"getPageElements","tags":["analysis"],"summary":"OCR layout elements (page fingerprint) for one file page","description":"Typed layout blocks from the parse-stage elements JSON (files/<fileId>/elements/p-<page>.json) — one entry per region the OCR model identified, in reading order, with normalized 0..1 bounding boxes. File-keyed and run-independent, like the page renders. 404 when the page has no elements JSON (older runs, parse failures) — clients should degrade quietly.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"},{"name":"fileId","in":"path","required":true,"schema":{"type":"string"}},{"name":"page","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Layout elements for the page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageElements"}}}},"400":{"description":"Invalid application id, file id or page number","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"No elements for this page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/placements":{"post":{"operationId":"recordPlacement","tags":["analysis"],"summary":"Manually file or archive an unassigned page range","description":"Filing for pages the analyzer left unassigned (\"your assignments win — Sheaf respects manual placement\"). A workflow deliberately does not file these: nothing recommended them, so they rest here for a human. target is a document expectedDocId on the pinned expected document list, or the literal \"archive\". Several ranges go in one request — the room junks a selection of pages at once — and are all recorded or none. A new placement replaces any earlier placement that overlaps the same page range. Recorded with decidedBy/decidedAt; the analyzer never writes these.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlacementInput"}}}},"responses":{"200":{"description":"Updated application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"400":{"description":"Invalid target expected document or page range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}},"/applications/{applicationId}/placements/withdraw":{"post":{"operationId":"withdrawPlacements","tags":["analysis"],"summary":"Withdraw the placements overlapping a span — a junked page comes back","description":"Removes every manual placement that shares a page with the span, so those pages return to wherever the latest run and the approval trail put them. Recorded with decidedBy; the analyzer never writes these.\n","parameters":[{"$ref":"#/components/parameters/ApplicationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlacementWithdrawal"}}}},"responses":{"200":{"description":"Updated application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Application"}}}},"400":{"description":"Invalid page range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}},"404":{"description":"Application not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiMessage"}}}}}}}},"components":{"securitySchemes":{"cookie":{"type":"apiKey","in":"cookie","name":"sheaf_session","description":"The signed session cookie POST /login sets — the app's identity."},"bearer":{"type":"http","scheme":"bearer","description":"An organization API key (`sheaf_…`) made in Settings → API keys. It acts as an Originator of that organization and cannot manage people, keys or passwords.\n"}},"parameters":{"ExpectedDocListId":{"name":"expectedDocListId","in":"path","required":true,"schema":{"type":"string"}},"Version":{"name":"version","in":"path","required":true,"schema":{"type":"integer"}},"DocType":{"name":"docType","in":"path","required":true,"schema":{"type":"string"}},"WorkflowId":{"name":"workflowId","in":"path","required":true,"schema":{"type":"string"}},"TemplateId":{"name":"templateId","in":"path","required":true,"schema":{"type":"string"}},"BriefId":{"name":"briefId","in":"path","required":true,"schema":{"type":"string"}},"ApplicationId":{"name":"applicationId","in":"path","required":true,"schema":{"type":"string"}},"ExpectedDocId":{"name":"expectedDocId","in":"path","required":true,"schema":{"type":"string"}}},"schemas":{"HealthStatus":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"]},"WorkflowRunInput":{"type":"object","properties":{"files":{"type":"array","description":"The packet — PDFs. Required when creating a case.","items":{"type":"string","format":"binary"}},"applicant":{"type":"string","description":"Applicant name — required unless applicationId is given."},"expectedDocList":{"type":"string","description":"`expectedDocListId` (its active version) or `expectedDocListId/version` — required unless applicationId is given."},"applicationId":{"type":"string","description":"Run on an existing case instead of creating one; files are optional then."},"decidedBy":{"type":"string","description":"Who consents to the run; defaults to the caller (an API key is `<name> (API)`)."}}},"GateRefused":{"type":"object","description":"The plain error envelope plus, when the case exists, its id; on a red-flag refusal the flags; on an account with no email yet, `emailRequired`.","required":["error"],"properties":{"error":{"type":"string"},"emailRequired":{"type":"boolean","description":"True when the refusal is a 403 because the organization is an instant account that has not added an email."},"applicationId":{"type":"string","description":"Present when the case exists — files were received; it waits at the gate."},"flags":{"type":"array","items":{"type":"object","required":["fileId","code","note"],"properties":{"fileId":{"type":"string"},"page":{"type":"integer"},"code":{"type":"string"},"note":{"type":"string"}}}}}},"ApiMessage":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"AccessMatrix":{"type":"object","required":["rights","roles","you"],"properties":{"rights":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"object","required":["role","abbr","rights"],"properties":{"role":{"type":"string"},"abbr":{"type":"string"},"rights":{"type":"array","items":{"type":"string"}}}}},"you":{"type":["string","null"],"description":"The caller's role"}}},"ContactInput":{"type":"object","required":["name","email","wants"],"properties":{"name":{"type":"string","maxLength":120},"email":{"type":"string","maxLength":200},"note":{"type":"string","maxLength":4000},"wants":{"type":"array","description":"What they would like — keys the server knows; unknown ones are dropped","items":{"type":"string"}},"company":{"type":"string","description":"Honeypot — humans never see it; a filled value is accepted with ok=true and dropped."}}},"ContactAck":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"AgentChatInput":{"type":"object","required":["messages"],"properties":{"messages":{"type":"array","description":"AI SDK UI messages, validated server-side","items":{"type":"object"}},"mode":{"type":"string","enum":["read","act"]}}},"ResolvedCitation":{"type":"object","required":["matched","boxes"],"properties":{"matched":{"type":"boolean"},"method":{"type":"string","enum":["exact","overlap"],"description":"how the match was made — exact normalized substring vs token overlap"},"boxes":{"type":"array","items":{"$ref":"#/components/schemas/PageElementBox"}}}},"PageElementBox":{"type":"object","description":"Normalized page-space rectangle — fractions 0..1 of page width/height, top-left origin.","required":["x","y","w","h"],"properties":{"x":{"type":"number"},"y":{"type":"number"},"w":{"type":"number"},"h":{"type":"number"}}},"PageElementBlock":{"type":"object","description":"One OCR layout region (Mistral block) in reading order.","required":["id","type","box","content","marginalia","sectionId"],"properties":{"id":{"type":"string","description":"Stable within the page — source block id when present, else b-<index>."},"type":{"type":"string","description":"OCR block label — text, title, list, table, image, equation, caption, code, references, aside_text, header, footer, signature. Open set; clients must tolerate unknown values.\n"},"box":{"$ref":"#/components/schemas/PageElementBox"},"content":{"type":"string","description":"Extracted markdown/text for the block."},"confidence":{"type":["number","null"],"description":"Model confidence 0..1 when the backend provides it."},"marginalia":{"type":"boolean","description":"Derived page-furniture ruling — true for header/footer typed blocks (positional rename) and for text-family blocks lying entirely inside the page's outer margin bands. Reader-computed from pure page-relative geometry at read time, never stored, so it applies retroactively to every run. Raw OCR `type` is preserved alongside.\n"},"sectionId":{"type":["string","null"],"description":"Derived visual grouping — consecutive column-aligned text-family blocks that read as one cluster share a section id (sec-<first member's block id>); null when ungrouped. Reader-computed pure geometry, never stored, retroactive to every run. Presentation-level only: blocks stay independent for verdicts, citations and grounding.\n"},"regionKind":{"type":["string","null"],"description":"Annotation-pass reclassification of table blocks — form_region (labeled fill-in fields) or data_table (static informational grid). Null when the page has no annotation pass or the block was not adjudicated. Open set; clients must tolerate unknown values.\n"},"fields":{"type":["array","null"],"description":"Extracted label/value pairs in reading order — form regions only since the table-native contract (checkbox states ride in the value, e.g. \"[x] no\"). Runs stored before the contract may still carry fields on data tables; clients should render those blocks as grids from `units` and ignore the fields.\n","items":{"$ref":"#/components/schemas/PageRegionField"}},"structure":{"type":["object","null"],"description":"Data-table grid semantics from the annotation pass — headerRows (leading column-header rows), columns (one short semantic name per column, in order) and sectionRows (0-based indices, headers included, of full-width section-label rows). Pure structure claims about the deterministic row units; never geometry. Null for form regions, unannotated blocks and pre-contract runs.\n","required":["headerRows","columns","sectionRows"],"properties":{"headerRows":{"type":"integer"},"columns":{"type":"array","items":{"type":"string"}},"sectionRows":{"type":"array","items":{"type":"integer"}}}},"units":{"type":["array","null"],"description":"Atomic sub-block units derived deterministically from the stored parse — table rows (kind \"row\", with cells) and text lines (kind \"line\"). Null when the annotation pass produced none for this block. Boxes ground exactly like field boxes; a unit without a box was not located on the page.\n","items":{"$ref":"#/components/schemas/PageBlockUnit"}},"figure":{"type":["object","null"],"description":"Image blocks only — which figure this is, and the parse model's annotation of it. imageId is the id in the block's markdown placeholder (![img-0.jpeg](img-0.jpeg)): no figure bytes are stored, so a client draws the figure from the page render at the block's box. width/height are the figure's size in the render's pixels (its aspect ratio and natural width). figureType and description are the model's guess (figure annotation pass), null when it produced none — show them as the model's words, never as the page's text. Null for every other block.\n","required":["imageId","width","height","figureType","description"],"properties":{"imageId":{"type":"string"},"width":{"type":"integer"},"height":{"type":"integer"},"figureType":{"type":["string","null"]},"description":{"type":["string","null"]}}}}},"PageRegionField":{"type":"object","description":"One extracted form field, as printed on the page.","required":["label","value"],"properties":{"label":{"type":"string"},"value":{"type":"string"},"box":{"type":"array","description":"Grounded page location of the field ([x0, y0, x1, y1], fractions of the page, top-left origin) — present only when the analyzer located the field's printed text among the page's atoms (PDF text layer, or local OCR on scanned pages). Geometry never comes from a model. Absent on pre-grounding runs and for fields whose text couldn't be located.\n","minItems":4,"maxItems":4,"items":{"type":"number"}}}},"PageBlockUnit":{"type":"object","description":"One atomic unit inside a block — a table row or a text line.","required":["kind","text"],"properties":{"kind":{"type":"string","description":"row (table row) or line (text-ish block line). Open set; clients must tolerate unknown values.\n"},"text":{"type":"string","description":"The unit's verbatim text from the stored parse."},"cells":{"type":"array","description":"Row units only — the row's cell texts in column order.","items":{"type":"string"}},"box":{"type":"array","description":"Grounded page location ([x0, y0, x1, y1], fractions of the page, top-left origin) — present only when the unit's text was located among the page's atoms (PDF text layer, or local OCR on scanned pages). Geometry never comes from a model.\n","minItems":4,"maxItems":4,"items":{"type":"number"}},"cellBoxes":{"type":"array","description":"Row units only — one entry per cell, parallel to `cells`: the cell's grounded box ([x0, y0, x1, y1], fractions of the page) or null when that cell's text wasn't located among the page atoms. Present only when at least one cell grounded. Same atom matching as `box`; geometry never comes from a model.\n","items":{"type":["array","null"],"minItems":4,"maxItems":4,"items":{"type":"number"}}}}},"PageElements":{"type":"object","description":"The page fingerprint — all layout blocks for one file page.","required":["page","blocks"],"properties":{"page":{"type":"integer"},"pageType":{"type":["string","null"],"description":"Document class of the page from the annotation pass — an RVL-CDIP-derived vocabulary trimmed for lending (form, statement, letter, id_document, contract, ...). Null for pages without an annotation pass. Open set; clients must tolerate unknown values.\n"},"grounding":{"type":["string","null"],"description":"Where field/unit geometry for this page came from — \"pdf-text-layer\" (born-digital page), \"ocr-tesseract\" (scanned page, atoms from local OCR — locations approximate), or \"none\" (no text layer and OCR produced nothing: boxes never appear). Null for pages without an annotation pass or from pre-grounding runs. Open set; clients must tolerate unknown values.\n"},"blocks":{"type":"array","items":{"$ref":"#/components/schemas/PageElementBlock"}}}},"ApplicationEvent":{"type":"object","description":"One ledger row — append-only, written with the state change it records.","required":["seq","at","actor","action"],"properties":{"seq":{"type":"number"},"at":{"type":"string"},"actor":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","description":"\"system\" covers automated transitions AND decisions made by a non-human DecisionActor (Workflow, Agent) — those cannot sign in, so recording them as \"user\" would misattribute the act.\n","enum":["user","system"]},"name":{"type":"string","description":"the actor who did it — for decisions, the decidedBy value"},"ip":{"type":"string"},"authenticatedAs":{"type":"string","description":"the signed-in identity whose session authorized the write. Differs from name exactly when a non-human actor decided; server-set, never client-supplied.\n"}}},"action":{"type":"string","description":"noun.verb, e.g. file.received, gate.confirmed, document.approved"},"target":{"type":"object","required":["type"],"properties":{"type":{"type":"string"},"id":{"type":"string"},"label":{"type":"string"}}},"detail":{"type":"object","additionalProperties":true}}},"ListApplicationEventsResponse":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationEvent"}},"ApplicationDocument":{"type":"object","description":"One document the application holds. `origin` says how it is known: `run` — the latest run parsed it, so `document` carries the live parse result (facts, plus the judge overlay when judged); `approved` — a human filed it under an earlier run and the latest run did not re-produce it, so it is history and carries no live parse.\nA run contributes both the documents it matched to the checklist and the ranges it matched to nothing. The unmatched ones are documents too — allocation is a priority signal, not a condition of existence, and on real files they are the majority. They are the rows with no `expectedDocId` and no `document`.\n","required":["address","title","spans","pages","origin","settled"],"properties":{"address":{"type":"string","description":"The canonical identity: \"<fileId>:p<first>-<last>\" per span, spans sorted by fileId then first page, joined by \"|\". Stable across runs for pages that did not move. There is deliberately no separate document id — see the endpoint description.\n"},"title":{"type":"string","description":"The document's own name, as the analyzer read it. A document the latest run did not produce is named by what it is filed under.\n"},"filedAs":{"type":"string","description":"The name of the requirement it is filed under or matched to; \"Not on the list\" for a document kept off the list. Absent for a range nobody has filed.\n"},"spans":{"type":"array","description":"The canonical address. Always address a document by this, never by `pages`.","items":{"$ref":"#/components/schemas/FileSpan"},"minItems":1},"pages":{"type":"array","description":"Inclusive 1-based [first, last] page numbers across the WHOLE application, counting active files in registry order. Derived for display and ordering only.\n","items":{"type":"integer"},"minItems":2,"maxItems":2},"origin":{"type":"string","enum":["run","approved"]},"expectedDocId":{"type":"string","description":"The expected document it is filed under; for an unsettled run document, the analyzer's suggestion; for an unmatched range a human has filed, the target they chose. Absent means nothing matched it — the common case on real files, not an error.\n"},"setId":{"type":"string","description":"set expected documents — which set it files into"},"unfiledCopy":{"type":"object","description":"The copy the run saved of exactly these pages at ingest, before anyone decided — a saved file (`unfiled_<read name>_<date>`, downloadable like an approved one), never a decision. Absent once a decision on the pages superseded it.\n","required":["approvedDocId","basename"],"properties":{"approvedDocId":{"type":"string"},"basename":{"type":"string"}}},"settled":{"type":"boolean","description":"A human has decided this document: an approval or a rejection of exactly these pages (the latest decision on the address), or — for a range that matched nothing — a manual placement filing it into an expected document. False means it still awaits a human, which is the common case. Ranges a human ARCHIVED as not-relevant are absent from the list entirely.\n"},"document":{"$ref":"#/components/schemas/RunDocument"},"approval":{"$ref":"#/components/schemas/DocumentApproval"}}},"ListApplicationDocumentsResponse":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationDocument"}},"ExpiryRule":{"type":"object","description":"Null means no clock. staleness uses days; hard means valid through closing.","required":["kind"],"properties":{"kind":{"type":"string","enum":["staleness","hard"]},"days":{"type":"integer"}}},"Field":{"type":"object","required":["id","type","label"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["text","number","date","select","yesno"]},"label":{"type":"string"},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"string"}}}},"ExpectedDoc":{"type":"object","description":"kind=document uses document fields; kind=fields uses the fields array.","required":["kind","id","name"],"properties":{"kind":{"type":"string","enum":["document","fields"]},"id":{"type":"string"},"name":{"type":"string"},"docType":{"type":"string","description":"Optional document-type id (e.g. bank_statement), shared by every expected document holding the same kind of document. Keys the extraction schema. Documents are matched against the expected documents themselves, never against this. Never applicant-facing.\n"},"docKind":{"type":"string","description":"The kind of document this is — the id of a root entry of the official catalogue (bank_statement, letter, cover_sheet, fallback). Required on a document-catalogue entry; copied onto a requirement with its type.\n"},"docTypeDescription":{"type":"string","description":"What the document looks like — the line the classifier reads."},"recognitionCues":{"type":"array","items":{"type":"string"},"description":"Printed phrases that tend to appear on one — headers, labels, form ids."},"structuralPageKind":{"type":"boolean","description":"A page that is not a document (blank page, cover sheet, separator, signature page, duplicate). Never a requirement on an ordinary list."},"junkRecommended":{"type":"boolean","description":"On a structural page kind — workflow mode archives such a range and the room offers Junk (blank and duplicate pages by default)."},"formats":{"type":"array","items":{"type":"string"}},"requirement":{"type":"string","enum":["required","required_alt","recommended","optional","recognition_only"],"description":"\"recognition_only\" means: not asked for, but recognisable. The expected document exists so the analyzer has something to file the document TO, and it is hidden from the applicant checklist — an applicant uploads one URLA package, not the five sub-documents inside it. It is the member half of a bundle (§4.2): the bundle is what is asked for, the members are what is recognised. Every other value is a real ask and appears on the checklist.\n"},"criticality":{"type":"string","enum":["critical","standard","supporting"]},"sourcing":{"type":"string","enum":["readily_available","constrained","scarce"]},"multiPage":{"type":"boolean"},"expiry":{"oneOf":[{"$ref":"#/components/schemas/ExpiryRule"},{"type":"null"}]},"arity":{"type":"string","enum":["single","sets"],"description":"\"sets\" = the requirement is satisfied by N sets (bank accounts, persons, …), each with its own documents. Absent = \"single\" (today's behavior).\n"},"setConfig":{"$ref":"#/components/schemas/SetConfig"},"analysisNote":{"type":"string","description":"Author-written expert guidance for the analyzer's satisfaction pass (e.g. \"statements must be consecutive; balance carryover should match\"). Never applicant-facing.\n"},"fields":{"type":"array","items":{"$ref":"#/components/schemas/Field"}}}},"SetConfig":{"type":"object","description":"Shape of a set expected document's sets. The expected document list declares WHAT identifies a set (descriptor field keys); the application supplies the actual sets per deal. Rules are data, never code (no conditional logic in v1).\n","required":["setNoun","descriptorFields","docsPerSet"],"properties":{"setNoun":{"type":"string","description":"What one set is called in the UI (\"Bank account\", \"Person\")."},"descriptorFields":{"type":"array","description":"Identity fields a set must fill, as specific as possible.","items":{"$ref":"#/components/schemas/DescriptorField"}},"docsPerSet":{"$ref":"#/components/schemas/DocsPerSet"}}},"DescriptorField":{"type":"object","required":["key","label"],"properties":{"key":{"type":"string","description":"stable snake_case key (institution, account_last4, name, dob)"},"label":{"type":"string","description":"UI label (\"Institution\", \"Account (last 4)\")"}}},"DocsPerSet":{"type":"object","required":["mode"],"properties":{"mode":{"type":"string","enum":["single","sequence"],"description":"single = exactly one document per set (birth certificate); sequence = one or more documents (statements). One document never fails a sequence — counts are guidance, not gates.\n"},"expectedCount":{"type":"integer","minimum":1,"description":"Recommended number of documents per set — guidance for review and the satisfaction pass, NEVER a hard limit or requirement.\n Document recency is governed by the expected document's existing expiry clock,\nnot duplicated here.\n"},"coverage":{"type":"string","enum":["consecutive_months"],"description":"optional — the sequence must cover consecutive periods with no gaps."}}},"Subsection":{"type":"object","required":["id","name","expectedDocs"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"bundleId":{"type":"string","description":"Set when this subsection was inserted from a catalogue bundle — the documents inside it ARE that bundle. Provenance, not a live link: the subsection is a copy, so later edits to the catalogue never rewrite an expected document list that already inserted it. Without this the subsection is an ordinary checklist category. Carries no verdict: a bundle subsection is still just a grouping.\n"},"expectedDocs":{"type":"array","items":{"$ref":"#/components/schemas/ExpectedDoc"}}}},"Permission":{"type":"object","required":["role","view","upload"],"properties":{"role":{"type":"string","enum":["Applicant","Originator","Underwriter","Manager"]},"view":{"type":"boolean"},"upload":{"type":"boolean"}}},"Section":{"type":"object","required":["id","name","owner","permissions","subsections"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"owner":{"type":"string","enum":["Applicant","Originator","Escrow","Homium"]},"permissions":{"type":"array","items":{"$ref":"#/components/schemas/Permission"}},"subsections":{"type":"array","items":{"$ref":"#/components/schemas/Subsection"}}}},"AlternativeGroup":{"type":"object","description":"Satisfied when any one of [primary, ...satisfiedBy] is filed.","required":["id","name","primary","satisfiedBy"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"primary":{"type":"string"},"satisfiedBy":{"type":"array","items":{"type":"string"}}}},"DocumentBundleMember":{"type":"object","description":"One SPECIFIC DOCUMENT inside a bundle.\n\n`id` is the member's identity and lives in the same space as an expected document id. That is what an insert matches on: an expected document list already has this document when it has an expected document with this id. It is minted ONCE from a name, by the same expected-doc-list-editor slug rule that mints expected document ids, and kept across renames — so relabelling either side never breaks the join, which matching on the display name could not promise.\n\nClient and server mint it with ONE shared implementation, precisely because two would silently drift (a truncation rule on one side only is enough) and a drifted id reads as \"the expected document list does not have this document\" — which re-creates it as recognition_only and strands the author's required copy.\n\n`name` is only the label shown to a human. `docType` names the SCHEMA the document is read with, and is deliberately allowed to repeat across members: a URLA package is five specific documents that all share the `application_form` schema. Doc types are a small, slow-moving set; specific documents number in the hundreds, so a bundle is built from documents and merely points at types.\n","required":["id","name","docType"],"properties":{"id":{"type":"string"},"name":{"type":"string","minLength":1},"docType":{"type":"string"}}},"DocumentBundleMemberInput":{"type":"object","description":"A member being written. Omit `id` for a document being added for the first time — the server mints it from the name and returns it. An existing member MUST send its id back unchanged; that is what makes the id survive a rename. A sent id is never re-derived and never repaired: one that is not a slug is refused, because minting a replacement would re-key a member other expected document lists already join against.\n","required":["name","docType"],"properties":{"id":{"type":"string"},"name":{"type":"string","minLength":1},"docType":{"type":"string"}}},"DocumentBundle":{"type":"object","description":"A catalogue bundle: a named, ordered list of specific documents that commonly arrive stapled together. Shared across every expected document list and authored on the Document Schema page. It holds REFERENCES ONLY — no requirement, no criticality, no fields. Those are per-document decisions and are made in the expected document list when the bundle is inserted as a subsection. A bundle is never satisfiable, has no verdict, and is never seen by the analyzer.\n","required":["id","name","members"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"members":{"type":"array","description":"the specific documents, in the order they appear inside the bundle","items":{"$ref":"#/components/schemas/DocumentBundleMember"}}}},"DocumentBundleInput":{"type":"object","required":["name","members"],"properties":{"name":{"type":"string","minLength":1},"members":{"type":"array","items":{"$ref":"#/components/schemas/DocumentBundleMemberInput"}}}},"ExpectedDocList":{"type":"object","required":["expectedDocList","version","status","program","alternatives","sections"],"properties":{"expectedDocListKind":{"$ref":"#/components/schemas/ExpectedDocListKind"},"extendsExpectedDocListId":{"type":"string","description":"An official document catalogue's parent list (`finance` extends `generic`): its entries are inherited, and an entry here with the same `docType` wins.\n"},"expectedDocList":{"type":"string"},"version":{"type":"integer"},"status":{"type":"string","enum":["draft","active","superseded"]},"program":{"type":"string"},"alternatives":{"type":"array","items":{"$ref":"#/components/schemas/AlternativeGroup"}},"sections":{"type":"array","items":{"$ref":"#/components/schemas/Section"}}}},"ExpectedDocListKind":{"type":"string","description":"What a list is and whose it is. `docTypeCatalogOfficial` and `expectedDocListCatalogOfficial` belong to the official organization and are read by every organization; `docTypeCatalogOrgCustom` and `expectedDocListCatalogOrgCustom` are an organization's own. Absent on a stored list = `expectedDocListCatalogOrgCustom`.\n","enum":["docTypeCatalogOfficial","docTypeCatalogOrgCustom","expectedDocListCatalogOfficial","expectedDocListCatalogOrgCustom"]},"ExpectedDocListListing":{"type":"object","required":["expectedDocListId","name","program","version","status","sections","docs","updated","inUseBy","expectedDocListKind","official","isDefault"],"properties":{"expectedDocListId":{"type":"string"},"name":{"type":"string"},"program":{"type":"string"},"version":{"type":"integer"},"status":{"type":"string","enum":["draft","active","superseded"]},"sections":{"type":"integer"},"docs":{"type":"integer"},"updated":{"type":"string"},"inUseBy":{"type":"integer"},"expectedDocListKind":{"$ref":"#/components/schemas/ExpectedDocListKind"},"official":{"type":"boolean","description":"The row belongs to the official organization (read-only for everyone else)."},"marked":{"type":"boolean","description":"On a `docTypeCatalogOfficial` list — the one this organization classifies against."},"isDefault":{"type":"boolean","description":"True on exactly one listing: the active version of the organization's default list — the list marked default in Classify while it has an active version, otherwise the most recently activated list.\n"}}},"ExpectedDocListRef":{"type":"object","required":["expectedDocListId","version"],"properties":{"expectedDocListId":{"type":"string"},"version":{"type":"integer"}}},"ExpectedDocListInput":{"type":"object","required":["name","program"],"properties":{"name":{"type":"string","minLength":1},"program":{"type":"string","minLength":1},"expectedDocListKind":{"type":"string","description":"Absent = the organization's own list. `expectedDocListCatalogOfficial` — a list template every organization sees and copies — is the official organization's to make (409 for any other).","enum":["expectedDocListCatalogOrgCustom","expectedDocListCatalogOfficial"]}}},"ExpectedDocListDuplicateInput":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1}}},"RenameInput":{"type":"object","description":"The display name to give the version being written.","required":["name"],"properties":{"name":{"type":"string","minLength":1}}},"SchemaFieldType":{"type":"string","description":"Same vocabulary as expected document list fields (text/number/date/select/yesno) plus the two container kinds extraction needs: object (named sub-fields) and list (repeating values or rows).\n","enum":["text","number","date","select","yesno","object","list"]},"SchemaField":{"type":"object","required":["id","name","type"],"properties":{"id":{"type":"string","description":"Stable node id — survives renames and reorders"},"name":{"type":"string","description":"Output key the extracted value is returned under"},"type":{"$ref":"#/components/schemas/SchemaFieldType"},"description":{"type":"string","description":"What the value means / where it tends to appear on the page"},"synonyms":{"type":"array","description":"Alternative labels real documents use for this field","items":{"type":"string"}},"required":{"type":"boolean"},"options":{"type":"array","description":"select only — the closed set of legal values","items":{"type":"string"}},"itemType":{"type":"string","description":"list of scalars only — the element type","enum":["text","number","date","yesno"]},"children":{"type":"array","description":"object — its named sub-fields; list — the shape of one row","items":{"$ref":"#/components/schemas/SchemaField"}}}},"DocTypeExtractionSchema":{"type":"object","description":"Per-document-type extraction schema. A document is read against the highest active version with fields down the ladder — its own type's, then its kind's, then `fallback` — the organization's before the official organization's at each step.\n","required":["docType","name","version","status","fields"],"properties":{"docType":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer"},"status":{"type":"string","enum":["draft","active","superseded"]},"fields":{"type":"array","items":{"$ref":"#/components/schemas/SchemaField"}}}},"WorkflowStepModels":{"type":"object","description":"Model option ids (GET /models/options) keyed by the WORKER STAGE they pin, not by tool — parse consumes two stages (parse and text), preview and judge one each. A step may only set stages its tool declares in WorkflowToolOption.modelStages.\n","properties":{"preview":{"type":"string"},"parse":{"type":"string"},"text":{"type":"string"},"judge":{"type":"string"}}},"WorkflowStepSettings":{"type":"object","description":"Boolean switches, under the same names the run routes already take. A step may only set settings its tool declares in WorkflowToolOption.settings.\n","properties":{"checklistInSegmentation":{"type":"boolean"},"pageAnnotation":{"type":"boolean"},"figureAnnotation":{"type":"boolean"},"fraudScoring":{"type":"boolean"}}},"WorkflowStep":{"type":"object","required":["tool"],"properties":{"tool":{"type":"string","description":"preview, parse, classify, a registered tool id, or intel (GET /workflow-tools). Parse is required; preview, when present, must precede it. Intel is not a tool: the API server answers its templates after the run is filed and its unfiled copies are saved.\n"},"models":{"$ref":"#/components/schemas/WorkflowStepModels"},"settings":{"$ref":"#/components/schemas/WorkflowStepSettings"},"intelTemplateIds":{"type":"array","items":{"type":"string"},"description":"The Intel templates the intel step answers, one brief each, once the run is filed. Only the intel step takes them, and it needs at least one; each must be an Intel template of this organization with an active version, checked on save and on activate. A brief reads the template's active version when the run lands.\n"}}},"Workflow":{"type":"object","description":"A named, versioned, reusable run recipe (workflow-mode plan §1). It is the library form of what `app.flow` already holds per application: applying one SEEDS that flow rather than adding a second execution path. Steps are ordered and linear because the pipeline is; there is deliberately no branching.\n","required":["workflowId","name","version","status","isDefault","steps"],"properties":{"workflowId":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer"},"status":{"type":"string","description":"Exactly one version of a workflow is `active`: activating a version supersedes the one it replaces, so resolving \"the workflow to run\" never has to choose between two candidates. `superseded` versions stay immutable — they describe runs that really happened.\n","enum":["draft","active","superseded"]},"isDefault":{"type":"boolean","description":"Server-owned. Set through POST /workflows/{workflowId}/set-default and IGNORED on save, so two clients cannot race a second default in.\n"},"steps":{"type":"array","items":{"$ref":"#/components/schemas/WorkflowStep"}}}},"WorkflowInput":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}},"WorkflowRef":{"type":"object","required":["workflowId","version"],"properties":{"workflowId":{"type":"string"},"version":{"type":"integer"}}},"WorkflowVersionListing":{"type":"object","required":["version","status","stepCount"],"properties":{"version":{"type":"integer"},"status":{"type":"string","enum":["draft","active","superseded"]},"stepCount":{"type":"integer"}}},"WorkflowListing":{"type":"object","required":["workflowId","name","isDefault","versions"],"properties":{"workflowId":{"type":"string"},"name":{"type":"string"},"isDefault":{"type":"boolean"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/WorkflowVersionListing"}}}},"IntelQuestion":{"type":"object","required":["id","text"],"properties":{"id":{"type":"string","description":"Stable within the template, across its versions; a brief's sections are keyed by it."},"text":{"type":"string","description":"The question, asked in plain words as a researcher would."}}},"IntelTemplate":{"type":"object","description":"A saved set of research questions answered over a whole case (intel-and-research plan §4.2). Versioned like the other libraries: a draft is editable, the one active version is immutable, and a brief answers the version active when it starts and records which.\n","required":["templateId","name","version","status","questions"],"properties":{"templateId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"integer"},"status":{"type":"string","enum":["draft","active","superseded"]},"questions":{"type":"array","description":"Up to 20; at least one to activate.","items":{"$ref":"#/components/schemas/IntelQuestion"}}}},"IntelTemplateInput":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}},"IntelTemplateRef":{"type":"object","required":["templateId","version"],"properties":{"templateId":{"type":"string"},"version":{"type":"integer"}}},"IntelTemplateVersionListing":{"type":"object","required":["version","status","questionCount","source"],"properties":{"version":{"type":"integer"},"source":{"$ref":"#/components/schemas/LibraryRowSource"},"status":{"type":"string","enum":["draft","active","superseded"]},"questionCount":{"type":"integer"}}},"IntelTemplateListing":{"type":"object","required":["templateId","name","versions"],"properties":{"templateId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/IntelTemplateVersionListing"}}}},"StartIntelBriefInput":{"type":"object","required":["templateId"],"properties":{"templateId":{"type":"string","description":"The brief answers this template's active version."},"readMode":{"$ref":"#/components/schemas/IntelReadMode"}}},"IntelReadMode":{"type":"string","enum":["auto","whole_case","search"],"default":"auto","description":"How a question reads the case. `whole_case` sends every page with the question; `search` lets the model search every line and read only the pages it needs, as citable pages; `auto` sends the whole case when it is under 200k tokens (about 150 pages) and searches above that.\n"},"IntelRead":{"type":"object","description":"What one question read — the mode it ran in, how many of the case's pages went in front of the model, and in search mode how many documents' facts it read.\n","required":["mode","pagesRead","pagesTotal"],"properties":{"mode":{"type":"string","enum":["whole_case","search"]},"pagesRead":{"type":"integer"},"pagesTotal":{"type":"integer"},"factsRead":{"type":"integer","description":"Search mode only — the documents whose facts (their extraction's cited values) the question read."}}},"IntelCitationElement":{"type":"object","description":"Where on the page: a Mistral block (`b-<index>`), and within it a unit — the index of a line or table row in the block's units — or null for the whole block (a figure). `cells` narrows a row to the cells the claim repeats, by index into the row's cells.\n","required":["blockId","unit"],"properties":{"blockId":{"type":"string"},"unit":{"type":["integer","null"]},"cells":{"type":"array","items":{"type":"integer"}}}},"IntelCitation":{"type":"object","description":"The finest citation only: a page and an element. Document and page totals are computed when read, so a split, merge or refile never leaves them stale.\n","required":["n","fileId","page","element","citedText","level"],"properties":{"n":{"type":"integer","description":"Its number within the brief; answer parts refer to it."},"fileId":{"type":"string"},"page":{"type":"integer"},"element":{"oneOf":[{"$ref":"#/components/schemas/IntelCitationElement"},{"type":"null"}]},"citedText":{"type":"string","description":"The cited unit's text as it was sent: a row is its cells joined by ` | `."},"level":{"type":"string","enum":["primary","supporting","conflicting"],"description":"conflicting — the answer line starts with \"Conflict:\"; primary — a figure or quoted phrase of the claim appears whole in the cited text; supporting — anything else.\n"}}},"IntelUsage":{"type":"object","required":["inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens"],"properties":{"inputTokens":{"type":"integer"},"outputTokens":{"type":"integer"},"cacheReadTokens":{"type":"integer"},"cacheWriteTokens":{"type":"integer"},"costUsd":{"type":"number","description":"Absent when a model that ran has no price on file."}}},"IntelAnswerPart":{"type":"object","required":["text","citations"],"properties":{"text":{"type":"string","description":"Markdown; parts concatenate into the answer."},"citations":{"type":"array","items":{"type":"integer"}}}},"IntelBriefSection":{"type":"object","required":["questionId","question","status","answer"],"properties":{"questionId":{"type":"string"},"question":{"type":"string"},"status":{"type":"string","enum":["pending","ready","failed"]},"error":{"type":"string"},"model":{"type":"string","description":"The model that answered — a fallback model when the pinned one declined."},"usage":{"$ref":"#/components/schemas/IntelUsage"},"read":{"$ref":"#/components/schemas/IntelRead"},"answer":{"type":"array","items":{"$ref":"#/components/schemas/IntelAnswerPart"}}}},"IntelPageNotRead":{"type":"object","required":["fileId","page","reason"],"properties":{"fileId":{"type":"string"},"page":{"type":"integer"},"reason":{"type":"string"}}},"IntelStartedBy":{"type":"object","required":["kind","name"],"properties":{"kind":{"type":"string","enum":["person","workflow"]},"name":{"type":"string"}}},"IntelBrief":{"type":"object","description":"One template answered for one case (intel-and-research plan §4.2). Read-only output: it files, approves and flags nothing. `running` until every section settles, then `ready`; `failed` only when it could not start or the server restarted under it.\n","required":["briefId","applicationId","templateId","templateVersion","templateName","status","startedAt","startedBy","pagesNotRead","sections","citations"],"properties":{"briefId":{"type":"string"},"applicationId":{"type":"string"},"templateId":{"type":"string"},"templateVersion":{"type":"integer"},"templateName":{"type":"string"},"status":{"type":"string","enum":["running","ready","failed"]},"error":{"type":"string"},"startedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time"},"startedBy":{"$ref":"#/components/schemas/IntelStartedBy"},"readMode":{"$ref":"#/components/schemas/IntelReadMode"},"pagesNotRead":{"type":"array","items":{"$ref":"#/components/schemas/IntelPageNotRead"}},"sections":{"type":"array","items":{"$ref":"#/components/schemas/IntelBriefSection"}},"citations":{"type":"array","items":{"$ref":"#/components/schemas/IntelCitation"}}}},"IntelBriefListing":{"type":"object","required":["briefId","templateId","templateVersion","templateName","status","startedAt","startedBy","questionCount","answeredCount"],"properties":{"briefId":{"type":"string"},"templateId":{"type":"string"},"templateVersion":{"type":"integer"},"templateName":{"type":"string"},"status":{"type":"string","enum":["running","ready","failed"]},"startedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time"},"startedBy":{"$ref":"#/components/schemas/IntelStartedBy"},"questionCount":{"type":"integer"},"answeredCount":{"type":"integer","description":"Sections ready so far."}}},"WorkflowToolOption":{"type":"object","description":"One authorable step kind. Derived from the tool registry — the spine (preview, parse, classify) first, then every registered tool — so a tool added to the registry becomes authorable with no edit to the workflows page.\n","required":["id","label","modelStages","settings"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"modelStages":{"type":"array","items":{"type":"string","enum":["preview","parse","text","judge"]}},"settings":{"type":"array","items":{"type":"string","enum":["checklistInSegmentation","pageAnnotation","figureAnnotation","fraudScoring"]}}}},"LibraryRowSource":{"type":"string","description":"Whose row this is — the official organization's (`official`, read-only for everyone else; \"new version\" on it starts the organization's own) or this organization's (`orgCustom`).","enum":["official","orgCustom"]},"DocTypeExtractionSchemaVersionInfo":{"type":"object","required":["version","status","fields","updated","source"],"properties":{"version":{"type":"integer"},"source":{"$ref":"#/components/schemas/LibraryRowSource"},"status":{"type":"string","enum":["draft","active","superseded"]},"fields":{"type":"integer","description":"recursive field count"},"updated":{"type":"string"}}},"DocumentTypeListing":{"type":"object","required":["id","name","docKind","definedIn","usedInExpectedDocLists","versions"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"docKind":{"type":"string","description":"The kind of document — a root entry of the marked chain."},"description":{"type":"string","description":"What the document looks like, from its catalogue entry."},"definedIn":{"type":"object","description":"The list whose entry defines this document — an official catalogue, or the organization's `custom-documents` — and its active version; where \"Edit document\" goes.","required":["expectedDocListId","version"],"properties":{"expectedDocListId":{"type":"string"},"version":{"type":"integer"}}},"usedInExpectedDocLists":{"type":"integer","description":"distinct expected document lists of this organization with an expected document of this type"},"versions":{"type":"array","description":"schema versions, newest first, the organization's own and the official organization's (`source`) — empty means the type resolves down the ladder (its kind's schema, then `fallback`)","items":{"$ref":"#/components/schemas/DocTypeExtractionSchemaVersionInfo"}}}},"CoinDocTypeInput":{"type":"object","required":["name","docKind"],"properties":{"name":{"type":"string","minLength":1},"docKind":{"type":"string","description":"The kind of document — the id of a root entry of the marked chain (`letter`, `report`, `fallback`)."}}},"DocTypeExtractionSchemaRef":{"type":"object","required":["docType","version"],"properties":{"docType":{"type":"string"},"version":{"type":"integer"}}},"DocTypeExtractionSchemaInput":{"type":"object","required":["name","docType"],"properties":{"name":{"type":"string","minLength":1},"docType":{"type":"string","description":"A document of the marked chain (coin one first with `POST /doc-types`); the schema's version 1 is created for it as a draft"}}},"SavedSection":{"type":"object","required":["id","name","source","section"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string"},"section":{"$ref":"#/components/schemas/Section"}}},"SavedSectionInput":{"type":"object","required":["name","source","section"],"properties":{"name":{"type":"string","minLength":1},"source":{"type":"string"},"section":{"$ref":"#/components/schemas/Section"}}},"WebhookEvent":{"type":"string","description":"A ledger action name. Templated actions are listed with their values.","enum":["application.created","file.received","file.renamed","file.archived","file.restored","closing-date.set","closing-date.cleared","fields.saved","expected-doc-list.repinned","inventory.renamed","gate.confirmed","gate.bypassed","parse.started","tool.judge.started","tool.extract.started","flow.judge.removed","flow.extract.removed","run.ingested","run.failed","run.cancelled","run.sidecars_failed","split.split","split.dismissed","merge.merged","merge.dismissed","placement.recorded","placement.withdrawn","document.approved","document.approved_incomplete","document.rejected","workflow.auto_filed","workflow.parse_skipped","workflow.extract_skipped","workflow.intel_skipped","set.added","set.deleted","intel_brief.started","intel_brief.ready","intel_brief.failed"]},"WebhookDelivery":{"type":"object","required":["at","status","attempt"],"properties":{"at":{"type":"string","format":"date-time","description":"When it was delivered, else when it was filed."},"status":{"type":"string","enum":["pending","delivered","failed"]},"attempt":{"type":"integer","description":"Attempts made so far (max 14)."},"responseCode":{"type":"integer"},"error":{"type":"string"}}},"WebhookEndpoint":{"type":"object","required":["id","url","events","active","createdBy","createdAt"],"properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","description":"Empty = every event.","items":{"$ref":"#/components/schemas/WebhookEvent"}},"active":{"type":"boolean","description":"False after a 410 from the receiver; add the endpoint again to resume."},"createdBy":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"lastDelivery":{"$ref":"#/components/schemas/WebhookDelivery"}}},"CreateWebhookInput":{"type":"object","required":["url"],"properties":{"url":{"type":"string","minLength":8,"maxLength":2000},"events":{"type":"array","description":"Ledger action names to deliver; omit or empty for every event.","items":{"$ref":"#/components/schemas/WebhookEvent"}}}},"CreatedWebhookEndpoint":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpoint"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"The signing secret, `whsec_` + base64. Shown once; verify deliveries with it."}}}]},"WebhookTestResult":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"},"status":{"type":"integer","description":"The receiver's HTTP status, when it answered."},"error":{"type":"string"}}},"ApiKey":{"type":"object","required":["id","name","prefix","createdBy","createdAt"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"prefix":{"type":"string","description":"The first characters of the secret, for telling keys apart."},"createdBy":{"type":"string","description":"Username of the Manager who minted it."},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":"string","format":"date-time","description":"Updated on every request the key makes."},"revokedAt":{"type":"string","format":"date-time","description":"Present once revoked; the key is refused from then on."}}},"CreateApiKeyInput":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80,"description":"What the key is for — the ledger names it \"<name> (API)\"."}}},"CreatedApiKey":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"The bearer token, `sheaf_` + 40 characters. Shown once; store it on your side."}}}]},"GuestAccess":{"type":"object","required":["key","applicationId","expiresAt","requestsPerMinute","pagesIncluded","docs"],"properties":{"key":{"type":"string","description":"The guest key — send it as `Authorization: Bearer …`. Shown once."},"applicationId":{"type":"string","description":"The sandbox's private copy of the sample case."},"expiresAt":{"type":"string","format":"date-time","description":"When the key is refused and the sandbox is deleted."},"requestsPerMinute":{"type":"integer"},"pagesIncluded":{"type":"integer","description":"Pages of uploads the sandbox may hold."},"docs":{"type":"string","description":"Where the API is documented."}}},"User":{"type":"object","required":["id","username","name","role","org","initials","active"],"properties":{"id":{"type":"string"},"username":{"type":"string","description":"The login identifier; accounts created since Sep 2026 use the lower-cased email."},"name":{"type":"string"},"role":{"type":"string","enum":["Originator","Underwriter","Manager"]},"org":{"type":"string","description":"The name of the organization this user belongs to."},"initials":{"type":"string"},"active":{"type":"boolean","description":"False once deactivated: the row stays (the ledger names it), sign-in is refused."},"unclaimedUntil":{"type":["string","null"],"format":"date-time","description":"Set while the organization is an instant account with no email yet: it is kept until then. Null once an email is added."}}},"LoginInput":{"type":"object","required":["username","password"],"properties":{"username":{"type":"string","minLength":1},"password":{"type":"string","minLength":1}}},"SignupInput":{"type":"object","properties":{"orgName":{"type":"string","minLength":2,"maxLength":80},"email":{"type":"string","minLength":3,"maxLength":200},"password":{"type":"string","minLength":10,"maxLength":200}}},"CreateUserInput":{"type":"object","required":["name","email","role","password"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","minLength":3,"maxLength":200},"role":{"type":"string","enum":["Originator","Underwriter","Manager"]},"password":{"type":"string","minLength":10,"maxLength":200,"description":"A temporary password the person changes on first sign-in."}}},"UpdateUserInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","minLength":3,"maxLength":200,"description":"The email the person signs in with from now on. Someone with no password yet is mailed a link to set one; adding the first email makes an instant account permanent."},"role":{"type":"string","enum":["Originator","Underwriter","Manager"]},"active":{"type":"boolean"},"password":{"type":"string","minLength":10,"maxLength":200,"description":"A new temporary password; ends the person's sessions."}}},"Organization":{"type":"object","required":["id","name","official"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"markedDocTypeCatalogId":{"type":"string","description":"The `expectedDocListId` of the official document catalogue this organization classifies against; absent = `generic`."},"official":{"type":"boolean","description":"True for the official organization — the oldest one — whose `docTypeCatalogOfficial` and `expectedDocListCatalogOfficial` lists every organization reads.\n"}}},"MarkedDocTypeCatalogInput":{"type":"object","required":["markedDocTypeCatalogId"],"properties":{"markedDocTypeCatalogId":{"type":"string","minLength":1}}},"MarkedDocTypeCatalog":{"type":"object","required":["expectedDocListId","entries"],"properties":{"expectedDocListId":{"type":"string"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/ExpectedDoc"}}}},"RenameOrganizationInput":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":2,"maxLength":80}}},"ChangePasswordInput":{"type":"object","required":["current","next"],"properties":{"current":{"type":"string","minLength":1},"next":{"type":"string","minLength":10,"maxLength":200}}},"ForgotPasswordInput":{"type":"object","required":["email"],"properties":{"email":{"type":"string","minLength":1,"maxLength":200}}},"ResetPasswordInput":{"type":"object","required":["token","password"],"properties":{"token":{"type":"string","minLength":1},"password":{"type":"string","minLength":10,"maxLength":200}}},"FieldValues":{"type":"object","description":"Map of field id to entered value for one field-group expected document.","required":["values"],"properties":{"values":{"type":"object","additionalProperties":{"type":"string"}}}},"SourceFile":{"type":"object","description":"One file as it entered the system — immutable, id-addressed bytes. Rename is metadata only. Files are never removed from the list; status is the only lifecycle field (archive behavior deferred).\n","required":["id","kind","origin","filename","sizeBytes","status","receivedAt"],"properties":{"id":{"type":"string","description":"sf-<nanoid8>, minted at intake, never reused"},"kind":{"type":"string","enum":["original","derived"]},"origin":{"type":"string","enum":["unsolicited","solicited"],"description":"solicited = uploaded against an expected document/set with declared intent"},"filename":{"type":"string","description":"current assigned name (rename = metadata)"},"originalFilename":{"type":"string","description":"as received (originals)"},"expectedDocId":{"type":"string","description":"solicited intent, if any"},"setId":{"type":"string"},"sizeBytes":{"type":"integer"},"pages":{"type":"integer","description":"PDFs only"},"sha256":{"type":"string"},"convertedFrom":{"type":"string","description":"original image filename when a jpg/png was wrapped into a single-page PDF at the receive seam; absent for native PDFs"},"flags":{"type":"array","description":"per-file deterministic pre-flight flags, computed at drop (fileId implicit)","items":{"$ref":"#/components/schemas/FileFlag"}},"status":{"type":"string","enum":["active","archived"],"description":"archived = out of the working view, never deleted (no UI yet — deferred)"},"receivedAt":{"type":"string"},"receivedBy":{"type":"string"},"receivedIp":{"type":"string"},"derivation":{"type":"object","description":"derived files only — lineage back to immutable sources","required":["op","sources"],"properties":{"op":{"type":"string","enum":["split","merge","crop"],"description":"crop — a region of one source page, cut from its render"},"sources":{"type":"array","items":{"type":"object","required":["fileId","pages"],"properties":{"fileId":{"type":"string"},"pages":{"type":"array","items":{"type":"integer"},"minItems":2,"maxItems":2},"box":{"$ref":"#/components/schemas/PageElementBox","description":"crop only — the region of the (single) source page, fractions of the page"}}}}}}}},"ReceiveFilesResponse":{"type":"object","required":["files","application"],"properties":{"files":{"type":"array","description":"the SourceFiles minted by THIS drop, in received order","items":{"$ref":"#/components/schemas/SourceFile"}},"application":{"$ref":"#/components/schemas/Application"}}},"UpdateSourceFileBody":{"type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":120,"description":"new assigned name — bytes and id unchanged, ledger records from→to"},"status":{"type":"string","enum":["active","archived"],"description":"archive/restore — bytes are never deleted; archived files leave the delta gate and runs"}}},"ApplicationSet":{"type":"object","description":"One real-world instance of a set expected document's requirement on THIS application. The expected document list declares the descriptor shape; the application holds the actual sets. Id minted once, never re-minted — uploads and (later) approvals key on it.\n","required":["id","descriptor","label","createdAt"],"properties":{"id":{"type":"string"},"descriptor":{"type":"object","description":"descriptorField key -> value, keys exactly as the expected document declares","additionalProperties":{"type":"string"}},"label":{"type":"string","description":"server-built display label (descriptor values joined)"},"createdAt":{"type":"string"}}},"ApprovedDocument":{"type":"object","description":"One approved, materialized document — the unit of the approved registry. Bytes live flat at approved/<applicationId>/<basename>.pdf + .md; this is the row that makes them findable. Append-only; re-acceptance supersedes (supersededBy), never deletes.\n","required":["id","applicationId","expectedDocId","basename","source","approvedBy","approvedAt"],"properties":{"id":{"type":"string"},"applicationId":{"type":"string"},"expectedDocId":{"type":"string"},"setId":{"type":"string","description":"set expected documents — which set this document belongs to"},"basename":{"type":"string","description":"shared basename of the .pdf/.md pair in the approved store"},"source":{"type":"string","enum":["extract","copy"],"description":"extract = spans pulled from source files; copy = direct intake upload copied whole"},"spans":{"type":"array","description":"the extracted file-qualified spans, in document order (extract only)","items":{"$ref":"#/components/schemas/FileSpan"}},"runId":{"type":"string","description":"analyzer run the assignment came from (extract only)"},"sourceFilename":{"type":"string","description":"original upload filename (copy only)"},"approvedBy":{"type":"string","description":"Who decided — a person's role, Workflow, or Analyzer for an unfiled copy the run saved at ingest before anyone decided (a saved copy, never a decision)."},"approvedAt":{"type":"string"},"supersededBy":{"type":"string","description":"id of the newer row that replaced this one — or `archive:<fileId>:p<f>-<l>` when a junk placement took every page of an unfiled copy"}}},"PageDecision":{"type":"object","description":"A per-page pre-step decision inside the document approval flow.","required":["fileId","page","decision"],"properties":{"fileId":{"type":"string"},"page":{"type":"integer","description":"1-based within the file"},"decision":{"type":"string","enum":["good","bad","flag_accepted"],"description":"good = page fine as-is; bad = page rejected; flag_accepted = page accepted despite flags/low scores — the flag stays on the record as a low-level note.\n"},"note":{"type":"string"}}},"DecisionActor":{"type":"string","description":"WHO DECIDED — the actor recorded on a decision's audit trail. Deliberately NOT the same vocabulary as User.role / Permission.role, which govern who MAY act (sign-in identity and the access matrix). Workflow (an authored chain running with no human in the loop) and Agent (an intelligent internal actor) can decide things but cannot sign in, so they must never be added to the identity enums.\n","enum":["Originator","Underwriter","Manager","Workflow","Agent"]},"DocumentApproval":{"type":"object","description":"A per-DOCUMENT approval (the filing decision) — the unit between page decisions and the checklist. Tagged to an expected document and, for set expected documents, a set. approved/approved_incomplete materialize into the approved registry through the same seam as expected document accepts; approved_incomplete additionally requests a new version. Append-only on the application; a re-approval of the same pages supersedes via the registry, never deletes.\n","required":["id","expectedDocId","runId","spans","outcome","decidedBy","decidedAt"],"properties":{"id":{"type":"string"},"expectedDocId":{"type":"string"},"setId":{"type":"string","description":"set expected documents — which set this document files into"},"runId":{"type":"string","description":"analyzer run whose grouping this approval is based on"},"spans":{"type":"array","description":"the approved document's pages as file-qualified spans, in reading order — each span ascending within itself, no two overlapping; a page moved earlier or later in the room is its own span (multi-span = also a human-accepted merge). The saved PDF, its markdown and the room follow this order.\n","items":{"$ref":"#/components/schemas/FileSpan"},"minItems":1},"pageDecisions":{"type":"array","items":{"$ref":"#/components/schemas/PageDecision"}},"outcome":{"type":"string","enum":["approved","approved_incomplete","rejected"]},"decidedBy":{"$ref":"#/components/schemas/DecisionActor"},"decidedAt":{"type":"string"},"authenticatedAs":{"type":"string","description":"the signed-in identity whose session authorized this approval. Equals decidedBy's human for a human filing; for a Workflow or Agent approval it is the only human on the record. Server-set from the request — never accepted from the client, or it would be forgeable and meaningless.\n"},"approvedDocId":{"type":"string","description":"registry row created by materialization (approved outcomes only)"}}},"DocumentApprovalInput":{"type":"object","required":["expectedDocId","runId","spans","outcome","decidedBy"],"properties":{"expectedDocId":{"type":"string"},"setId":{"type":"string"},"runId":{"type":"string"},"spans":{"type":"array","items":{"$ref":"#/components/schemas/FileSpan"},"minItems":1},"pageDecisions":{"type":"array","items":{"$ref":"#/components/schemas/PageDecision"}},"outcome":{"type":"string","enum":["approved","approved_incomplete","rejected"]},"decidedBy":{"$ref":"#/components/schemas/DecisionActor"}}},"MergeResolution":{"type":"object","description":"The decision on an analyzer merge recommendation between two run groups. Keyed on the application by \"<runId>:<fileId>:p<f>-<l>|<fileId>:p<f>-<l>\" (spans sorted by fileId then first page). Latest decision wins — reversible.\n","required":["runId","spans","decision","decidedBy","decidedAt"],"properties":{"runId":{"type":"string"},"spans":{"type":"array","description":"the two file-qualified spans the recommendation covers","items":{"$ref":"#/components/schemas/FileSpan"},"minItems":2,"maxItems":2},"decision":{"type":"string","enum":["merged","dismissed"]},"decidedBy":{"$ref":"#/components/schemas/DecisionActor"},"decidedAt":{"type":"string"}}},"MergeResolutionInput":{"type":"object","required":["runId","spans","decision","decidedBy"],"properties":{"runId":{"type":"string"},"spans":{"type":"array","items":{"$ref":"#/components/schemas/FileSpan"},"minItems":2,"maxItems":2},"decision":{"type":"string","enum":["merged","dismissed"]},"decidedBy":{"$ref":"#/components/schemas/DecisionActor"}}},"SplitResolution":{"type":"object","description":"The decision on an analyzer split recommendation — the mirror of MergeResolution: merge joins two groups, split divides one. Keyed on the application by \"<fileId>:p<f>-<l>\", the span of the document the recommendation was made ABOUT. Files are immutable, so that span identifies the recommendation across runs and the decision survives later delta runs. Latest decision wins — reversible.\n","required":["runId","span","decision","decidedBy","decidedAt"],"properties":{"runId":{"type":"string"},"span":{"$ref":"#/components/schemas/FileSpan"},"decision":{"type":"string","enum":["split","dismissed"]},"decidedBy":{"$ref":"#/components/schemas/DecisionActor"},"decidedAt":{"type":"string"}}},"SplitResolutionInput":{"type":"object","required":["runId","span","decision","decidedBy"],"properties":{"runId":{"type":"string"},"span":{"$ref":"#/components/schemas/FileSpan"},"decision":{"type":"string","enum":["split","dismissed"]},"decidedBy":{"$ref":"#/components/schemas/DecisionActor"}}},"ApplicationInput":{"type":"object","required":["expectedDocListId","version","applicantName"],"properties":{"expectedDocListId":{"type":"string"},"version":{"type":"integer"},"applicantName":{"type":"string","minLength":1},"projectedClosingDate":{"type":"string"}}},"ApplicationUpdate":{"type":"object","properties":{"projectedClosingDate":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"ApplicationSummary":{"type":"object","required":["id","expectedDocListId","version","expectedDocListName","applicantName","createdAt","docsFiled","docsTotal"],"properties":{"id":{"type":"string"},"expectedDocListId":{"type":"string"},"version":{"type":"integer"},"expectedDocListName":{"type":"string"},"applicantName":{"type":"string"},"createdAt":{"type":"string"},"docsFiled":{"type":"integer"},"docsTotal":{"type":"integer"},"projectedClosingDate":{"type":"string"}}},"SampleCase":{"type":"object","required":["id","title","blurb","applicantName","docsFiled","docsTotal","files","pages"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"blurb":{"type":"string"},"applicantName":{"type":"string"},"docsFiled":{"type":"integer"},"docsTotal":{"type":"integer"},"files":{"type":"integer"},"pages":{"type":"integer"}}},"SampleCaseScratch":{"type":"object","required":["applicationId","expiresAt","reviewRoomPath"],"properties":{"applicationId":{"type":"string"},"expiresAt":{"type":"string"},"reviewRoomPath":{"type":"string","description":"Where the app goes — the scratch's review room, with the sample's opening when it has one (`/applications/{id}/review?step=parse&view=page`)."}}},"Application":{"type":"object","required":["id","expectedDocListId","version","applicantName","createdAt","fieldValues","expectedDocList","storageFolder"],"properties":{"id":{"type":"string"},"storageFolder":{"type":"string","description":"Human-legible App Storage folder for this application, frozen at creation: yyyymm_surname_initials_<id>. Never re-derived from the applicant name — renames/corrections do not move bytes.\n"},"expectedDocListId":{"type":"string"},"version":{"type":"integer"},"applicantName":{"type":"string"},"createdAt":{"type":"string"},"fieldValues":{"type":"object","description":"expectedDocId -> field values map","additionalProperties":{"type":"object","additionalProperties":{"type":"string"}}},"projectedClosingDate":{"type":"string"},"sample":{"type":"object","description":"Set by the operator script on a curated case — it is then offered to every signed-in person as a sample.","required":["title","blurb","order"],"properties":{"title":{"type":"string"},"blurb":{"type":"string"},"order":{"type":"integer"},"open":{"type":"string","description":"How the review room opens for this sample — its query string, e.g. `step=parse&view=page` (the page with its fingerprint showing). Absent = the room's ordinary landing."}}},"sampleOf":{"type":"string","description":"Present on a sample-case scratch — the id of the sample it was opened from. A scratch shares the sample's files, takes no uploads and no runs, and appears in no list."},"scratchOwnerId":{"type":"string","description":"The user a scratch belongs to."},"scratchExpiresAt":{"type":"string","description":"When a scratch's rows are swept (ISO 8601, like the document's other timestamps)."},"run":{"$ref":"#/components/schemas/RunState"},"flow":{"$ref":"#/components/schemas/ApplicationFlow"},"files":{"type":"array","description":"SourceFile registry (file-native intake phase 2) — every file that entered this application, append-only in spirit. filename and status are the only mutable fields.\n","items":{"$ref":"#/components/schemas/SourceFile"}},"expectedDocList":{"$ref":"#/components/schemas/ExpectedDocList"},"expectedDocListHistory":{"type":"array","description":"Audit trail of expected document list re-pins (who, when, vN→vN)","items":{"$ref":"#/components/schemas/ExpectedDocListRepinEvent"}},"materializationErrors":{"type":"object","description":"expectedDocId -> last approval-materialization failure (loud, with reason). The approval stands; the approved registry row is missing until a successful retry clears the entry. Portal-owned.\n","additionalProperties":{"type":"object","required":["message","at"],"properties":{"message":{"type":"string"},"at":{"type":"string"}}}},"documentApprovals":{"type":"array","description":"Per-document approvals from the filmstrip flow, append-only, newest first. The registry (approved-docs) is the materialized truth; this is the decision trail on the application. Portal-owned.\n","items":{"$ref":"#/components/schemas/DocumentApproval"}},"mergeResolutions":{"type":"object","description":"Merge-recommendation decisions, keyed \"<runId>:p<f>-<l>|p<f>-<l>\". A PENDING recommendation (no entry) gates approval of both groups. Portal-owned.\n","additionalProperties":{"$ref":"#/components/schemas/MergeResolution"}},"splitResolutions":{"type":"object","description":"Split-recommendation decisions, keyed \"<fileId>:p<f>-<l>\" — the span of the document the recommendation was about. A PENDING recommendation (no entry) gates approval of that document, the same way a pending merge gates both its groups. Portal-owned.\n","additionalProperties":{"$ref":"#/components/schemas/SplitResolution"}},"sets":{"type":"object","description":"expectedDocId -> sets of that set expected document on this application. Intake-side data — sets and their files are NOT part of the application's satisfied requirements until a human accepts.\n","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationSet"}}},"manualPlacements":{"type":"array","description":"Human filings of analyzer-unassigned page ranges (portal-owned)","items":{"$ref":"#/components/schemas/ManualPlacement"}}}},"ExpectedDocListUpgradeInput":{"type":"object","required":["targetVersion","decidedBy"],"properties":{"targetVersion":{"type":"integer"},"decidedBy":{"type":"string"}}},"ExpectedDocListRepinEvent":{"type":"object","required":["fromVersion","toVersion","decidedBy","decidedAt"],"properties":{"fromVersion":{"type":"integer"},"toVersion":{"type":"integer"},"decidedBy":{"type":"string"},"decidedAt":{"type":"string"}}},"ManualPlacement":{"type":"object","description":"A decision about an unassigned page range: filed into an expected document, or archived as not-relevant. Waits for the next analyzer run to confirm — manual placement always wins over analyzer suggestions.\n","required":["span","target","decidedBy","decidedAt"],"properties":{"span":{"$ref":"#/components/schemas/FileSpan"},"target":{"type":"string","description":"expectedDocId on the pinned expected document list, or the literal \"archive\""},"note":{"type":"string"},"decidedBy":{"$ref":"#/components/schemas/DecisionActor"},"decidedAt":{"type":"string"},"runId":{"type":"string"}}},"PlacementInput":{"type":"object","required":["spans","target","decidedBy"],"properties":{"spans":{"type":"array","minItems":1,"description":"the page ranges placed, one or more — recorded in one write, each as its own placement; an archive re-cuts the unfiled copies once, after all of them\n","items":{"$ref":"#/components/schemas/FileSpan"}},"target":{"type":"string","description":"expectedDocId on the pinned expected document list, or the literal \"archive\""},"note":{"type":"string"},"decidedBy":{"$ref":"#/components/schemas/DecisionActor"},"runId":{"type":"string"}}},"PlacementWithdrawal":{"type":"object","required":["span","decidedBy"],"properties":{"span":{"$ref":"#/components/schemas/FileSpan"},"decidedBy":{"$ref":"#/components/schemas/DecisionActor"}}},"FileSpan":{"type":"object","description":"The canonical page address: a contiguous, inclusive 1-based page range WITHIN one SourceFile. A span never crosses a file boundary; a document spanning files carries multiple spans.\n","required":["fileId","pages"],"properties":{"fileId":{"type":"string"},"pages":{"type":"array","description":"[first, last] inclusive, 1-based within the file","items":{"type":"integer"},"minItems":2,"maxItems":2}}},"FileFlag":{"type":"object","description":"Structured deterministic pre-flight flag. fileId is omitted where the file is implicit (SourceFile.flags); page is omitted for whole-file flags. note carries the human-readable sentence.\n","required":["code","note"],"properties":{"fileId":{"type":"string"},"page":{"type":"integer","description":"1-based within the file"},"code":{"type":"string","description":"machine code — blank, duplicate, low_dpi, low_contrast, ..."},"note":{"type":"string"}}},"RunInputFile":{"type":"object","description":"one file in a run's frozen input set","required":["fileId","filename","sha256","pages"],"properties":{"fileId":{"type":"string"},"filename":{"type":"string","description":"assigned name at kick time (display)"},"sha256":{"type":"string"},"pages":{"type":"integer"}}},"SkippedInputFile":{"type":"object","description":"One input file a run could not prepare (fetch / render / atoms / parse raised). The run still lands everything it COULD read — this record is why the rest is missing.\n","required":["fileId","filename","reason"],"properties":{"fileId":{"type":"string"},"filename":{"type":"string"},"reason":{"type":"string","description":"the failure, verbatim (exception type + message, truncated)"}}},"InventoryEntry":{"type":"object","description":"One proposed document from the preview stage (light pre-parse): a span, a type guess with its printed evidence, and a provisional name. Identity is the SPAN (entryKey fileId:pFIRST-LAST) — re-previews merge by span so a user-edited name can never be orphaned. The inventory proposes; the full parse decides — no values are extracted at preview.\n","required":["span","docTypeGuess","suggestedExpectedDocId","confidence","evidence","readState","nameSource"],"properties":{"span":{"$ref":"#/components/schemas/FileSpan"},"docTypeGuess":{"type":"string","description":"The matched expected document id, or \"unassigned\" — never an invented type.\n"},"suggestedExpectedDocId":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The pinned expected document this segment matches; null when nothing matches (legal here, unlike documents[] — the confirm step surfaces exactly these). Several entries MAY share one expected document: a packet that correctly splits into seven parts of one checklist item is seven documents, and nulling six of them would make a right answer look wrong.\n"},"confidence":{"type":"number"},"evidence":{"type":"string","description":"the printed phrase / signal that drove the guess (auditable)"},"readState":{"type":"string","enum":["reading","read","failed"],"description":"How much of this row is real. Three states, not a boolean, because \"still being read\" and \"could not be read\" must never render the same as each other OR as a result (absent is not zero).\nreading = the instant local sketch: one row per file, filename and full page span, posted so the review room can open while the file is still being read. Nothing about it has been analyzed and the scan replaces it wholesale, so it is inert — no rename (the server refuses it), no allocation, no match verdict. A parse is refused while any row is `reading`: a scan delta landing mid-parse would union over the documents that parse just paid for.\nread = a real scanned result, and the only state that may be treated as an analysis of the document.\nfailed = the file's scan errored. Span and name are still only the filename and the page count, and suggestedExpectedDocId is null with confidence 0 because nothing was learned — the row exists so the file is visibly accounted for rather than silently missing, and so it can be sent to the parse as one document. It must render as a failure, never as an unassigned document.\n"},"provisionalName":{"type":"string","description":"display name until the full parse confirms — free to edit on /review"},"nameSource":{"type":"string","enum":["filename","text_layer","model","user","derived"],"description":"filename = the sketch's placeholder, derived locally from the uploaded file name · text_layer = exact, read from the page · model = a label the split model proposed · user = hand-edited, wins permanently (the server refuses any model or re-preview overwrite) · derived = computed by us from the page itself, with no model involved, for a page the segmentation left out of its answer: \"Blank page\", \"Duplicate of page 3\", or \"Unplaced page\". Preview completes its own page coverage rather than rejecting an incomplete answer, because voiding a whole file over two junk pages hides the document the human came to look at.\n"}}},"RunGateDecision":{"type":"object","required":["decision","decidedAt"],"properties":{"decision":{"type":"string","enum":["auto","confirmed","bypassed"]},"decidedBy":{"type":"string","description":"Absent when decision=auto; otherwise the signed-in staff profile."},"decidedAt":{"type":"string"},"authenticatedAs":{"type":"string","description":"The signed-in user whose session authorized this run. Server-set, never client-supplied. Carried because a workflow run decides things AFTER the human has gone: ingest arrives under the analyzer worker's service identity, so the automated approvals it triggers attribute the decision to Workflow and the authorization to this person.\n"},"workflow":{"$ref":"#/components/schemas/WorkflowRef"}}},"RunGateInput":{"type":"object","description":"The gate decision starts ONE of two entries over the gated file set. mode=preview (default) runs the light pre-parse into an inventory and spends almost nothing. mode=parse skips preview entirely and runs the PAID full parse over every gated file WHOLE (one span per file, pages 1..N) — there is no inventory to select from, so the gate click IS the spend consent. settings and tools apply to mode=parse only and fall back to the application's flow defaults, exactly as at the inventory step. plan applies to BOTH entries, each reading only its own stages: mode=preview reads `preview` and `checklistInSegmentation`, mode=parse reads parse/text/judge and `fraudScoring`.\n","required":["decision","decidedBy"],"properties":{"decision":{"type":"string","enum":["confirmed","bypassed"]},"decidedBy":{"type":"string"},"mode":{"type":"string","enum":["preview","parse"],"default":"preview","description":"preview = inventory first (cheap, always stops for a human). parse = straight to the paid full parse over the whole gated set.\n"},"fileIds":{"type":"array","minItems":1,"items":{"type":"string"},"description":"Only these files, each waiting at the gate — a named file that is not (unknown, archived, or already covered) is a 409 naming it. Omitted: every waiting file. The decision matches the named files' own flags. The review room's Extract region sends its one new file here, so the region is read the moment it is cut.\n"},"tools":{"type":"array","items":{"type":"string","enum":["judge"]},"description":"Post-parse tools chained into the SAME mode=parse kick — the whole sequence on one click. [] = parse only; [\"judge\"] = parse then judge. Absent falls back to the application's flow (whatever later scoped parses would auto-chain), so an omitted field never silently changes an established flow.\nChaining judge here also RECORDS it into the flow, exactly as the untargeted \"+ Tool\" backfill does (tool-runs §2c: an unscoped consent is the flow-add) — so every later parse keeps chaining it. This is the only entry that can arm judge before a parse has landed; \"+ Tool\" needs parsed facts to backfill over.\n"},"plan":{"$ref":"#/components/schemas/RunPlan"},"settings":{"$ref":"#/components/schemas/ParseSettings"},"workflow":{"$ref":"#/components/schemas/WorkflowRef"}}},"StartScopedParseInput":{"type":"object","required":["entryKeys","decidedBy"],"properties":{"entryKeys":{"type":"array","minItems":1,"items":{"type":"string"},"description":"inventory entry keys (fileId:pFIRST-LAST) selecting the spans to parse"},"decidedBy":{"type":"string"},"plan":{"$ref":"#/components/schemas/RunPlan"},"settings":{"$ref":"#/components/schemas/ParseSettings"}}},"RenameInventoryEntryInput":{"type":"object","required":["provisionalName"],"properties":{"provisionalName":{"type":"string","minLength":1,"maxLength":120}}},"RunPlan":{"type":"object","description":"Per-stage model option ids (from GET /models/options) for THIS run. Omitted stages use the system default. The worker resolves ids at run start and fails loudly on unknown/unavailable options — never a silent engine substitution; the resolved plan is frozen into the run's config artifact (pipelineVersion honesty).\nA run reads only the stages its lane uses: a preview run reads `preview`, a parse run reads parse/text/judge. A stage a lane never touches is never resolved, so a missing preview credential cannot block a parse.\n","properties":{"preview":{"type":"string"},"parse":{"type":"string"},"text":{"type":"string"},"judge":{"type":"string"},"fraudScoring":{"type":"boolean","description":"Per-run addon toggle (default true): score every judged document for visual fraud indicators. Off = fraud_signal is ABSENT from this run's scores and the UI renders \"not scored this run\" — never a fake 0. Frozen into the run's config artifact like the engine ids.\n"},"checklistInSegmentation":{"type":"boolean","description":"Per-run preview toggle: show the application's pinned expected document list to preview's segmentation prompt, letting it name each proposed document against a checklist id. The gate sends it on every preview kick, ticked by default. Off, or absent, = the prompt sees page text only and every requirement reads unmatched. Titles are read off the page either way.\n"}}},"ParseSettings":{"type":"object","description":"Parse output settings (tool-runs plan §2f). Engine natives (tables, hyperlinks, header/footer classification, paragraph bboxes/labels, confidence) are always on; these are the add-on riders. An absent field means ON for a caller that sends nothing; the app's own dialogs default page annotation OFF and figure annotation ON (Sep 23 2026: the page rider is 2–3.5× the per-page latency on scans, and only the review room's page-type badge and form field list read it — row and line units keep their boxes either way). Both are request-time-only parameters at the provider, so changing them later means a surgical re-parse of the affected spans (re-parse is an explicitly supported move).\nSchema-directed extraction is NOT here: it is its own step, started after a human has accepted or reassigned the run's document recommendations (POST /run/tool with tool=extract).\n","properties":{"pageAnnotation":{"type":"boolean","description":"per-page structured annotation (page_type + form/table adjudication + form fields); absent = on, the app's dialogs default off"},"figureAnnotation":{"type":"boolean","description":"per-figure bbox annotation (figure transcripts); default true"}}},"ToolParseConfig":{"type":"object","description":"frozen Parse choice for this application's flow","properties":{"model":{"type":"string","description":"parse model option id (GET /models/options)"},"text":{"type":"string","description":"text model option id (satisfaction/naming passes)"},"settings":{"$ref":"#/components/schemas/ParseSettings"}}},"ToolJudgeConfig":{"type":"object","description":"frozen Judge choice for this application's flow","properties":{"model":{"type":"string","description":"judge model option id"},"fraudScoring":{"type":"boolean"}}},"StartToolRunInput":{"type":"object","required":["tool","decidedBy"],"properties":{"tool":{"type":"string","enum":["judge","extract"]},"decidedBy":{"type":"string"},"targets":{"type":"array","minItems":1,"description":"JUDGE ONLY — spans of parsed documents to re-judge, each entry equalling an existing document's span exactly. Absent = the consented backfill (every parsed document lacking the overlay). Sending targets with tool=extract is a 400; extract selects with `subjects`, because a span is not enough to name a document that no parsed record backs.\n","items":{"$ref":"#/components/schemas/FileSpan"}},"subjects":{"type":"array","minItems":1,"description":"EXTRACT ONLY — document ADDRESSES to read, drawn from the application's extractable set (the latest run's documents, its unassigned ranges and the approved filing history, with the human's split and merge resolutions applied). Absent = every extractable document.\nAddresses, not spans: the caller SELECTS, the portal decides what a document is and which requirement it is filed under. An address the server does not resolve is a 400, never a silent skip — a partial pass that quietly dropped a document would leave the room unable to tell \"not read\" from \"refused\".\n","items":{"type":"string"}},"plan":{"$ref":"#/components/schemas/ToolJudgeConfig"}}},"ApplicationFlow":{"type":"object","description":"The application's tool flow (tool-runs plan): which workflow tools run after Parse, with their frozen configs — uniform scrutiny across every document of the case. v1 tools: judge. Absent tool = not run (absent ≠ zero; approving unjudged documents is legal). Every later scoped parse auto-chains the enabled tools over its spans.\n","properties":{"parse":{"$ref":"#/components/schemas/ToolParseConfig"},"judge":{"$ref":"#/components/schemas/ToolJudgeConfig"}}},"ModelOption":{"type":"object","required":["id","label","status","available","default"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"status":{"type":"string","enum":["validated","experimental"]},"note":{"type":"string"},"available":{"type":"boolean"},"unavailableReason":{"type":"string"},"default":{"type":"boolean"}}},"ModelStageOptions":{"type":"object","required":["stage","locked","options"],"properties":{"stage":{"type":"string","enum":["parse","text","judge","preview"]},"locked":{"type":"boolean","description":"Locked stages render but cannot be changed (judge in v1, spec §1.2)."},"options":{"type":"array","items":{"$ref":"#/components/schemas/ModelOption"}}}},"ModelOptionsResponse":{"type":"object","required":["stages"],"properties":{"stages":{"type":"array","items":{"$ref":"#/components/schemas/ModelStageOptions"}}}},"RunProgress":{"type":"object","description":"Live \"where is it up to\" for an in-flight run, reported by the analyzer worker as it advances. SELF-DESCRIBING on purpose: the worker declares the ordered steps of the stage it is in and each step's state and count, so nothing downstream learns the pipeline. The portal stores this verbatim; the client renders one line per step. A new stage or step needs no portal or client change (a client label is optional wording).\nAbsent ≠ zero: no progress record means the worker has not reported yet (\"waiting\"), NOT that nothing is done.\n","required":["stage","sequence","steps","updatedAt"],"properties":{"stage":{"type":"string","description":"the worker's own stage name (parse | classify | judge | extract)"},"sequence":{"type":"integer","description":"increases with every snapshot the worker takes, across every stage of one run. Pings are sent concurrently (a page lands while another is still posting), so the portal keeps only a snapshot newer than the one it holds — the display never steps backwards.\n"},"steps":{"type":"array","description":"the stage's steps in the order they run","items":{"$ref":"#/components/schemas/RunProgressStep"}},"updatedAt":{"type":"string","format":"date-time","description":"when the worker last reported — a change, or its heartbeat, which repeats the latest snapshot every 30 s while the run is alive. The client shows \"no update in Xm\" rather than a frozen count that still looks alive, and the portal stops a paid run silent for 3 minutes.\n"}}},"RunProgressStep":{"type":"object","description":"One step of the running stage. `completed`, `total` and `unit` come together or not at all: a step with no meaningful count (drawing the boxes, saving the run) carries only its state.\n","required":["id","state"],"properties":{"id":{"type":"string","description":"the worker's own step name (render_pages, read_pages, ...)"},"state":{"type":"string","enum":["pending","running","done"]},"completed":{"type":"integer","description":"units finished so far"},"total":{"type":"integer","description":"units in this step"},"unit":{"type":"string","description":"what is being counted (pages | spans | documents)"}}},"RunProgressReport":{"description":"One progress ping from the analyzer worker — a RunProgress plus the requestId that proves which kick it belongs to.\n","allOf":[{"$ref":"#/components/schemas/RunProgress"},{"type":"object","required":["requestId"],"properties":{"requestId":{"type":"string","description":"the kick this ping belongs to (stale-worker guard)"}}}]},"RunProgressAck":{"type":"object","description":"Always 200 — `stored` false means a guard dropped the ping (no run in flight, a different requestId, or an older sequence), which is normal and not an error the worker should react to.\n","required":["stored"],"properties":{"stored":{"type":"boolean"}}},"RunFailure":{"type":"object","required":["reason","requestId"],"properties":{"reason":{"type":"string"},"requestId":{"type":"string","description":"the kick this failure belongs to (stale-worker guard)"}}},"RunState":{"type":"object","description":"Portal-owned run lifecycle over the ACTIVE FILE SET (file-native runs). Two entries, both human-clicked. Preview-first: gated → previewing → inventory → running → report, where the inventory step waits for an explicit click and /run/parse starts the paid scoped parse (plus the flow's chained tools) over selected spans only. Direct: gated → running → report, where the gate itself starts a whole-set parse (prior=gated, so a failure lands back at the gate rather than at an inventory that never existed). The run lifecycle never advances itself; a workflow run's RESULTS, by contrast, are filed and approved automatically once it lands. running covers any paid tool run; kind says which. Files land durably at drop; the gate covers the set, not a blob. Persisted server-side so the gate physically blocks.\n","required":["state"],"properties":{"state":{"type":"string","enum":["gated","previewing","inventory","running","report"]},"kind":{"type":"string","enum":["parse","classify","judge","extract"],"description":"which step is running (present while state=running)"},"prior":{"type":"string","enum":["gated","inventory","report"],"description":"the state this tool run started from (present while state=running); a failed run reverts HERE — a judge backfill kicked from report must not strand the application at inventory, and a parse started straight from the gate must land back at the gate, having produced no inventory to return to\n"},"requestId":{"type":"string","description":"minted per kick; guards the failure callback against stale workers"},"input":{"type":"array","description":"the frozen input set of the in-flight/last run","items":{"$ref":"#/components/schemas/RunInputFile"}},"gate":{"$ref":"#/components/schemas/RunGateDecision"},"lastRunError":{"type":"string","description":"Plain-language reason the last analyzer kick failed (reverted to gated). Cleared by the next gate decision or run ingest.\n"},"progress":{"$ref":"#/components/schemas/RunProgress"}}},"AnalysisExtractKeyData":{"type":"object","description":"Universal core fields — the four every document has regardless of type, produced by the PARSE lane's match call (not the judge: they are facts, and they arrive on unjudged runs). Type-specific values live in `extraction`, read against that type's authored schema. Ungrounded in v1 — the verdict UI must display the dates for human confirmation at accept. Key names follow the analyzer contract verbatim.\n","required":["document_date","primary_party_name"],"properties":{"document_date":{"type":"string"},"expiry_date":{"type":"string"},"primary_party_name":{"type":"string"},"issuing_party":{"type":"string"}}},"AnalysisScores":{"type":"object","required":["quality","formatting","scrutinyTier"],"properties":{"quality":{"type":"number"},"formatting":{"type":"number"},"fraud_signal":{"type":"number","description":"Absent when the run's plan disabled fraud scoring (RunPlan.fraudScoring = false) — render \"not scored this run\", never assume 0.\n"},"scrutinyTier":{"type":"string","enum":["critical","standard","supporting"]}}},"AnalysisFlag":{"type":"object","required":["code","detail"],"properties":{"code":{"type":"string"},"detail":{"type":"string"}}},"AnalysisJudgeOverlay":{"type":"object","description":"The Judge tool's opinions about one document — an OVERLAY on the parse facts (tool-runs doctrine: parse = facts, judge = opinions). Absent = not judged, and absent ≠ zero: the UI says \"not judged\", never fakes a score, and approving unjudged documents is legal. A re-judge replaces the whole overlay (latest wins).\n","required":["scores","flags","at"],"properties":{"scores":{"$ref":"#/components/schemas/AnalysisScores"},"flags":{"type":"array","items":{"$ref":"#/components/schemas/AnalysisFlag"}},"description":{"type":"string","description":"the judge's one-line read of the document (audit trail)"},"model":{"type":"string","description":"judge model option id (provenance)"},"promptVersion":{"type":"string"},"at":{"type":"string"}}},"AnalysisArtifacts":{"type":"object","description":"Provenance references for this document's per-page artifacts. md and pageRenders are application-relative object-storage keys (files/<fileId>/md|pages/p-<n>...); judge stays a run-store reference.\n","required":["md","pageRenders","crops"],"properties":{"judge":{"type":"string","description":"Audit-grade raw judge output for this document (judge/doc-<N>.json in the run's store folder) — the pre-mapping superset that every rendered score traces back to. Additive; absent on older runs.\n"},"md":{"type":"array","description":"One key per page of the document's span, in page order — the same shape as pageRenders. Previously a single key pointing at the span's FIRST page, which made a 20-page document advertise a one-page transcript.\n","items":{"type":"string"}},"pageRenders":{"type":"array","items":{"type":"string"}},"crops":{"type":"array","items":{"type":"string"}}}},"RunDocument":{"type":"object","description":"One parsed document — IDENTITY + FACTS (tool-runs doctrine). Scores, flags and description are the Judge tool's opinions and live in the optional judge overlay; a document with no overlay was parsed but not judged.\n","required":["spans","suggestedExpectedDocId","suggestedName","extractKeyData","artifacts"],"properties":{"spans":{"type":"array","description":"the document's pages as file-qualified spans, in reading order — ascending, non-overlapping; multiple spans = the analyzer read one document across ranges/files (merge recommendation surface)\n","items":{"$ref":"#/components/schemas/FileSpan"},"minItems":1},"suggestedExpectedDocId":{"type":"string","description":"Must resolve to an expected document in the application's pinned expected document list."},"docKind":{"type":"string","description":"The kind of document this is — a `docKind` of the marked catalogue (the root entries), implied by the requirement it satisfies; `fallback` when nothing fits.\n"},"confidence":{"type":"number","description":"A measured self-report, when a stage produced one — ABSENT when nothing measured it. A direct checklist match yields no calibrated number, so the room shows no figure rather than a low one.\n"},"suggestedName":{"type":"string","description":"Derived, never hand-written. Human rename always wins and is recorded."},"extractKeyData":{"$ref":"#/components/schemas/AnalysisExtractKeyData"},"descriptors":{"type":"object","description":"SET expected documents only — the set identity the matcher read off THIS document, keys a subset of the expected document's setConfig.descriptorFields. Facts, not a placement: the portal matches them against the deal's declared sets itself, so a read the deal never declared files nothing. Absent when the expected document is not a set, or nothing legible was printed — a partial read is kept, since it still names the pile.\n","additionalProperties":{"type":"string"}},"recommendedSplit":{"$ref":"#/components/schemas/AnalysisRecommendedSplit"},"judge":{"$ref":"#/components/schemas/AnalysisJudgeOverlay"},"artifacts":{"$ref":"#/components/schemas/AnalysisArtifacts"},"splitFrom":{"$ref":"#/components/schemas/FileSpan","description":"On a piece of a confirmed split only — the span it was cut from. Present when a person approved the split or a workflow followed it; absent on everything the analyzer produced as such.\n"}}},"AnalysisExtraction":{"type":"object","description":"Schema-directed extraction for ONE document: the fields its own type declares, read against the authored schema that was active when the extract step went out — or against the Default schema when that type has none of its own (Default is the FALLBACK, never a floor added on top of a specific schema). The four universal core fields are not repeated here; they ride extractKeyData, which the match call already produced.\nAddressed by SPAN, not hung off a parsed document. A document's identity is its address, and the extractable ones include an unassigned range, an approved split piece, a merged pair and a document filed under an earlier run — none of which has an RunDocument record to hang an overlay on. ABSENT FROM run.extractions means the extract step has not read that address yet — never that a read came back empty.\n","required":["address","spans","docType","schemaVersion","values","notStated"],"properties":{"address":{"type":"string","description":"Canonical document address this read is about — \"fileId:pF-L\" joined by \"|\" for a multi-span document. The key the ingest union merges on: a later read of the same address replaces the earlier one.\n"},"spans":{"type":"array","minItems":1,"description":"The pages read. Redundant against `address` by construction, and kept anyway so the record stays self-describing once the run that produced it is old and the document has been re-cut underneath it.\n","items":{"$ref":"#/components/schemas/FileSpan"}},"docType":{"type":"string","description":"id of the authored schema this document was read against"},"schemaVersion":{"type":"integer","description":"Version of that schema. Recorded because with active authoring a stored value is uninterpretable later without knowing what was asked for at the time.\n"},"values":{"type":"object","description":"One entry per declared field id — nested objects for object fields, arrays for list fields. null = the document does not state it.\n","additionalProperties":true},"notStated":{"type":"array","description":"Declared field ids the document is silent on. The explicit form of \"asked, and it is not there\", which absent ≠ zero requires be distinguishable from a field nobody asked for.\n","items":{"type":"string"}},"ignoredKeys":{"type":"array","description":"Keys the model returned that the schema never declared — dropped before storage, reported so invention is visible rather than silent.\n","items":{"type":"string"}},"sources":{"type":"object","description":"Where each stated value was read, by field path — `principal`, `borrower.name`, `fees[0].amount` (list items from 0) — as Claude's search-result citations mapped to the parse's own lines. A source only points at a line the parse already recorded: no new text, no new location. Absent on reads from before extraction cited (Sep 2026).\n","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/ExtractionSource"}}}}},"ExtractionSource":{"type":"object","description":"One line of the parse a value was read from — the element shape Intel citations use.","required":["fileId","page","element","citedText"],"properties":{"fileId":{"type":"string"},"page":{"type":"integer"},"element":{"$ref":"#/components/schemas/IntelCitationElement"},"citedText":{"type":"string","description":"The line's text when the value was read; the viewer draws its box only while the page still holds that text.\n"}}},"AnalysisRecommendedSplit":{"type":"array","description":"Where the analyzer would cut this document into independent documents. A RECOMMENDATION: the default managed document is the whole raw file (or the whole target range). Once a person approves the split, or a workflow follows it, every read of the run shows the pieces in this document's place (each with splitFrom). Splits are virtual — no PDF is chopped, nothing is re-parsed, and the stored run is unchanged.\nPresent only when the analyzer read MORE THAN ONE document here, hence minItems 2: a one-piece \"split\" is not a recommendation, it is the document, and it is left absent rather than stated as a no-op.\n","items":{"$ref":"#/components/schemas/AnalysisSplitPiece"},"minItems":2},"AnalysisSplitPiece":{"type":"object","description":"One piece of a recommended split — what the analyzer WOULD have called a document had it applied the boundary it read. Each piece carries the identity it would have, so approving a split is immediate and needs no re-parse: the read already happened, it is simply not acted on until a person approves the split or a workflow follows it.\n","required":["span","suggestedName","extractKeyData","description"],"properties":{"span":{"$ref":"#/components/schemas/FileSpan"},"suggestedExpectedDocId":{"type":"string","description":"The checklist requirement this piece would satisfy. ABSENT when no requirement matched — absent is not zero, and a piece matching nothing is still a real piece worth cutting out before the expected document list has an expected document for it.\n"},"suggestedName":{"type":"string","description":"Derived, never hand-written."},"docKind":{"type":"string","description":"The kind of document this piece is — a `docKind` of the marked catalogue, implied by the requirement it satisfies when one does; `fallback` when nothing fits.\n"},"recommendedJunk":{"type":"boolean","description":"The piece's kind carries `junkRecommended` (blank page and duplicate page by default): applied, the split leaves it as a range to junk — workflow mode archives it, human mode offers Junk.\n"},"extractKeyData":{"$ref":"#/components/schemas/AnalysisExtractKeyData"},"description":{"type":"string","description":"One line, what this piece is — the read a human checks."},"descriptors":{"type":"object","description":"SET expected documents only — the set identity read off THIS piece, keys a subset of the expected document's setConfig.descriptorFields. Carried per piece for the same reason a whole document carries it: applying a split has to know WHICH pile each piece belongs to, and the parent's read describes the bundle, not the piece. Absent when the piece's expected document is not a set, or nothing legible was printed.\n","additionalProperties":{"type":"string"}}}},"AnalysisUnassigned":{"type":"object","description":"A range no checklist requirement matched. It is still a document the user uploaded, so it carries the SAME read a matched document carries — allocation is a priority signal, not a condition of existence. On real files the unmatched case is the common one (29 of 32 pieces in a 104-page closing package), which is exactly where the read is worth keeping.\n","required":["span","description"],"properties":{"span":{"$ref":"#/components/schemas/FileSpan"},"description":{"type":"string"},"docKind":{"type":"string","description":"The kind of document this range is — a `docKind` of the marked catalogue, answered although no requirement matched; `fallback` when nothing fits.\n"},"recommendedJunk":{"type":"boolean","description":"The range's kind carries `junkRecommended` (blank page and duplicate page by default): workflow mode archives it, human mode offers Junk.\n"},"suggestedName":{"type":"string","description":"The filename the classify read wrote for it. Allocation is identical for every unmatched range (all null), so the name is the entire user-visible difference between one and the next.\n"},"extractKeyData":{"type":"object","description":"Universal core fields read off the range (document_date, primary_party_name, and issuing_party/expiry_date when printed) — the same bag a matched document carries.\n","additionalProperties":{"type":"string"}},"recommendedSplit":{"$ref":"#/components/schemas/AnalysisRecommendedSplit"},"splitFrom":{"$ref":"#/components/schemas/FileSpan","description":"On a piece of a confirmed split only — the span it was cut from. Present when a person approved the split or a workflow followed it; absent on everything the analyzer produced as such.\n"}}},"AnalysisPreflight":{"type":"object","required":["pages","flags","gate"],"properties":{"pages":{"type":"integer","description":"total pages across the input set"},"flags":{"type":"array","description":"structured pre-flight flags across the input set (fileId-qualified)","items":{"$ref":"#/components/schemas/FileFlag"}},"gate":{"type":"string","enum":["auto","confirmed","bypassed"]}}},"SatisfactionGroup":{"type":"object","description":"one set-shaped pile of run documents, as the pass read them","required":["docSpans"],"properties":{"setId":{"type":"string","description":"declared set this pile belongs to, when the pass could tell"},"descriptorGuess":{"type":"object","description":"descriptor values the pass read off the documents (keys = the expected document's descriptorFields)","additionalProperties":{"type":"string"}},"docSpans":{"type":"array","description":"each run document in this pile, as its file-qualified spans","items":{"type":"array","items":{"$ref":"#/components/schemas/FileSpan"}}},"coverage":{"type":"string","description":"human sentence on sequence coverage (periods present, holes)"}}},"ExpectedDocSatisfaction":{"type":"object","required":["groups","gaps","summary","generatedAt"],"properties":{"groups":{"type":"array","items":{"$ref":"#/components/schemas/SatisfactionGroup"}},"gaps":{"type":"array","description":"what is still missing, one human sentence each (empty = nothing obvious)","items":{"type":"string"}},"summary":{"type":"string","description":"one paragraph — the expert read of where this requirement stands"},"model":{"type":"string","description":"text model that produced this (provenance)"},"generatedAt":{"type":"string"}}},"AnalysisRun":{"type":"object","required":["runId","kind","startedAt","pipelineVersion","input","preflight","documents","unassigned","whisper"],"properties":{"runId":{"type":"string"},"requestId":{"type":"string","description":"The kick this run answers (stale-worker guard at ingest: the state flip only happens when this matches the application's current run.requestId).\n"},"input":{"type":"array","description":"the frozen file set this run analyzed","items":{"$ref":"#/components/schemas/RunInputFile"}},"skippedInputs":{"type":"array","description":"Input files this run could not read. A parse run is partial-success: one unreadable file no longer voids the files that already parsed (and billed), so the run lands with the rest and names what it skipped. Selected spans on a skipped file are absent from targets — they were never worked, so the union carries any prior results for them instead of replacing them with nothing. Absent = nothing was skipped; the union carries a prior skip forward only while the newer run left that file untouched.\n","items":{"$ref":"#/components/schemas/SkippedInputFile"}},"startedAt":{"type":"string"},"pipelineVersion":{"type":"string"},"durationMs":{"type":"integer","description":"Total wall time of the run in ms (additive telemetry — the speed axis for engine comparisons). Per-stage timings live in the run store's config.json.\n"},"artifactsProduced":{"type":"object","description":"Per-engine honesty — which artifact kinds THIS run actually produced (bare-VLM parses emit md only; Paddle/Mistral also emit elements). UI reads this instead of assuming capabilities.\n","required":["md","elements","crops"],"properties":{"md":{"type":"boolean"},"elements":{"type":"boolean"},"crops":{"type":"boolean"},"annotations":{"type":"boolean","description":"Page annotations (page_type + form/table adjudication) — Mistral parses only. Absent on pre-annotation runs = false.\n"},"atoms":{"type":"boolean","description":"Text-layer atoms (word/line geometry from the original PDF — KV grounding now, extraction highlights next). Absent on pre-grounding runs = false.\n"}}},"fraudScoring":{"type":"boolean","description":"Echo of the resolved plan's fraud toggle. false = fraud_signal is absent on every document of THIS run (\"not scored this run\"). Absent on older runs = scored.\n"},"preflight":{"$ref":"#/components/schemas/AnalysisPreflight"},"documents":{"type":"array","items":{"$ref":"#/components/schemas/RunDocument"}},"unassigned":{"type":"array","items":{"$ref":"#/components/schemas/AnalysisUnassigned"}},"extractions":{"type":"array","description":"Schema-directed reads, keyed by document ADDRESS rather than hung off a parsed document — an unassigned range, an approved split piece, a merged pair and a document filed under an earlier run are all extractable, and none of them has an RunDocument record. Latest read per address wins at ingest; the union carries every address the newer run did not re-read. Absent on runs from before extraction moved off the document.\n","items":{"$ref":"#/components/schemas/AnalysisExtraction"}},"satisfaction":{"type":"object","description":"Satisfaction pass (Phase 4) — per SET expected document with ≥1 assigned document, a text-LLM read of how the run's documents map onto the expected document's declared sets and rules. Assistive only, never a gate; humans re-assign freely. Absent on runs before the pass existed or when no set expected document had documents.\n","additionalProperties":{"$ref":"#/components/schemas/ExpectedDocSatisfaction"}},"whisper":{"type":"array","items":{"type":"string"}},"kind":{"type":"string","enum":["preview","parse","classify","judge","extract"],"description":"Which step produced this run. preview = light pre-parse: documents/ unassigned stay empty and inventory is populated; ingest flips previewing→inventory. parse = the paid scoped parse — pages and page facts ONLY. It posts NO documents: a document is a claim about what something is, and parse makes no such claim. A chained parse also posts without a requestId, so the lifecycle flip waits for the classify that follows it. classify = boundaries, allocation, identity and naming, read off markdown parse already stored; zero OCR spend, so re-composing an application is free. It decides WHICH DOCUMENTS EXIST, so it lands on the coverage-delta ingest arm, never the overlay arm. judge = a judge-only re-run over already-parsed documents (overlay replacement). extract = schema-directed extraction over already-parsed documents, read against the type each one is CURRENTLY filed under (overlay replacement, zero parse spend).\n"},"targets":{"type":"array","description":"The spans this run actually worked (a scoped run from the inventory step). The ingest union keeps prior documents that do NOT overlap these spans — span-granular, so per-entry runs of one file never clobber each other. Absent = the run covered its whole input.\n","items":{"$ref":"#/components/schemas/FileSpan"}},"inventory":{"type":"array","description":"Preview-stage output — the confirm-not-configure audit trail /review shows before real spend. Entry identity is the span; a re-preview merges by span and user-edited names win permanently.\n","items":{"$ref":"#/components/schemas/InventoryEntry"}}}},"AnalysisSidecar":{"type":"object","description":"Portal-owned sidecar file (data/analysis/<applicationId>.json). Append-only runs.","required":["applicationId","latestRunId","runs"],"properties":{"applicationId":{"type":"string"},"latestRunId":{"oneOf":[{"type":"string"},{"type":"null"}]},"runs":{"type":"array","items":{"$ref":"#/components/schemas/AnalysisRun"}}}}}}}