{"info":{"description":"Generated from the live routing table; request/response schemas from the verified endpoint dataset. Errors follow RFC-7807 (problem+json).","title":"VForce Lakehouse Platform API","version":"v1"},"openapi":"3.0.3","paths":{"/api/v1/access-requests":{"get":{"description":"Lists access requests visible to the caller. With ?mine=true it returns the caller's own requests in any status; otherwise it returns the approver queue: pending requests for tables on which the caller holds asset-management (unmask) authority. Results are sorted by creation time, newest first.","requestBody":{"content":{"application/json":{"schema":{"properties":{"mine":{"description":"Query param; when exactly 'true', returns the caller's own requests instead of the approver queue.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"requests":[{"created_at":"2026-07-15T14:22:05Z","id":"9f2a1c7b4e6d0a83","reason":"Need read access to build the Q3 revenue dashboard.","requester":"alice@edgentllc.com","status":"pending","table":"analytics.sales.orders"}]},"schema":{"properties":{"requests":{"description":"Array of accessRequest objects (id, requester, table, reason, status, decided_by, decided_at, created_at), sorted by created_at descending; empty array when none match.","items":{"type":"object"},"type":"array"}},"required":["requests"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists access requests visible to the caller.","tags":["Access requests"]},"post":{"description":"Files a new pending access request for a dataset on behalf of the calling subject. Anyone may request; the target table must be a valid catalog.schema.table FQN, and the free-text reason is truncated to 2000 characters before the request is persisted to the tenant-scoped object store.","requestBody":{"content":{"application/json":{"example":{"reason":"Need read access to build the Q3 revenue dashboard.","table":"analytics.sales.orders"},"schema":{"properties":{"reason":{"description":"Optional justification for the access; silently truncated to 2000 characters.","type":"string"},"table":{"description":"Fully-qualified target table as catalog.schema.table; each segment must be a safe SQL identifier or the request is rejected 400.","type":"string"}},"required":["table"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-15T14:22:05Z","id":"9f2a1c7b4e6d0a83","reason":"Need read access to build the Q3 revenue dashboard.","requester":"alice@edgentllc.com","status":"pending","table":"analytics.sales.orders"},"schema":{"properties":{"created_at":{"description":"RFC3339 UTC creation timestamp.","type":"string"},"decided_at":{"description":"RFC3339 UTC decision timestamp; omitted while pending.","type":"string"},"decided_by":{"description":"Subject that decided the request; omitted while pending.","type":"string"},"id":{"description":"Generated 16-hex-char request identifier.","type":"string"},"reason":{"description":"The (possibly truncated) reason; omitted when empty.","type":"string"},"requester":{"description":"Subject that filed the request (derived server-side from the authenticated caller; 'anonymous' if none).","type":"string"},"status":{"description":"Lifecycle status; always 'pending' on creation (later 'approved' or 'denied').","type":"string"},"table":{"description":"The requested table FQN.","type":"string"}},"required":["id","requester","table","status","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Files a new pending access request for a dataset on behalf of the calling subject.","tags":["Access requests"]}},"/api/v1/access-requests/tenant":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Access requests"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Access requests"],"x-stability":"experimental"}},"/api/v1/access-requests/{id}/decide":{"post":{"description":"Approves or denies a pending access request. The caller must hold asset-management (unmask) authority on the request's table. On approval, the requester is granted the 'viewer' relation on the table via the OpenFGA grant hook; on denial the request is simply marked denied. The decided request is persisted with the deciding subject and timestamp.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"decision":"approve"},"schema":{"properties":{"decision":{"description":"Either 'approve' or 'deny'; any other value is rejected 400.","type":"string"}},"required":["decision"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-15T14:22:05Z","decided_at":"2026-07-15T15:03:41Z","decided_by":"bob@edgentllc.com","id":"9f2a1c7b4e6d0a83","reason":"Need read access to build the Q3 revenue dashboard.","requester":"alice@edgentllc.com","status":"approved","table":"analytics.sales.orders"},"schema":{"properties":{"created_at":{"description":"Original RFC3339 UTC creation timestamp.","type":"string"},"decided_at":{"description":"RFC3339 UTC decision timestamp.","type":"string"},"decided_by":{"description":"Subject that made the decision (the calling approver).","type":"string"},"id":{"description":"Request identifier.","type":"string"},"reason":{"description":"Original justification; omitted when empty.","type":"string"},"requester":{"description":"Subject that filed the request.","type":"string"},"status":{"description":"Updated status: 'approved' or 'denied'.","type":"string"},"table":{"description":"The requested table FQN.","type":"string"}},"required":["id","requester","table","status","decided_by","decided_at","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Approves or denies a pending access request.","tags":["Access requests"]}},"/api/v1/admin/authz/backfill-parents":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/admin/authz/grant":{"post":{"description":"Seeds a single ReBAC relationship by writing one raw OpenFGA tuple: it grants principal `user` the `relation` on `object`. This is the operator seam used to bootstrap access in the demo — grant a group `viewer` on a catalog, add a user as a `member` of a group, grant a table-level `reader`, or wire a parent edge — writing exactly the tuple supplied with no normalization.","requestBody":{"content":{"application/json":{"example":{"object":"catalog:cedms","relation":"viewer","user":"group:analysts#member"},"schema":{"properties":{"object":{"description":"Typed FGA object ref the relation is written on, e.g. `catalog:cedms`, `group:analysts`, `table:cedms.microfiche.pages`.","type":"string"},"relation":{"description":"Relation to grant, e.g. `viewer`, `reader`, `member`, `admin`, `owner`, or `parent`.","type":"string"},"user":{"description":"FGA principal ref, e.g. `user:analyst`, `group:analysts#member` (userset), or `user:bob`.","type":"string"}},"required":["user","relation","object"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"granted":{"object":"catalog:cedms","relation":"viewer","user":"group:analysts#member"}},"schema":{"properties":{"granted":{"description":"Echo of the written tuple as an object {user, relation, object} (all strings).","type":"object"}},"required":["granted"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Seeds a single ReBAC relationship by writing one raw OpenFGA tuple: it grants principal `user` the `relation` on `object`.","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/admin/authz/query":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/admin/authz/revoke":{"post":{"description":"Rescinds a ReBAC grant by deleting one OpenFGA tuple: it removes principal `user`'s `relation` on `object`. Part of the catalog Manage console governance surface; the operation is idempotent, so revoking a grant that does not exist still returns 200.","requestBody":{"content":{"application/json":{"example":{"object":"table:cedms.microfiche.pages","relation":"viewer","user":"user:bob"},"schema":{"properties":{"object":{"description":"Typed FGA object ref the tuple is on, e.g. `table:cedms.microfiche.pages`.","type":"string"},"relation":{"description":"Relation to remove, e.g. `viewer`.","type":"string"},"user":{"description":"FGA principal whose grant is being removed, e.g. `user:bob`.","type":"string"}},"required":["user","relation","object"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"revoked":{"object":"table:cedms.microfiche.pages","relation":"viewer","user":"user:bob"}},"schema":{"properties":{"revoked":{"description":"Echo of the removed tuple as an object {user, relation, object} (all strings).","type":"object"}},"required":["revoked"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Rescinds a ReBAC grant by deleting one OpenFGA tuple: it removes principal `user`'s `relation` on `object`.","tags":["Admin"]}},"/api/v1/admin/authz/transfer-owner":{"post":{"description":"Transfers ownership of a catalog to a new principal by swapping the FGA `owner` tuple. Ownership is the root destructive authority (owner implies admin, cascading to the catalog's schemas and tables), so the server sequences grant-new-then-revoke-old to guarantee the catalog is never ownerless, and returns the new owner plus the list of previous owners that were rescinded.","requestBody":{"content":{"application/json":{"example":{"new_owner":"alice@edgentllc.com","object":"catalog:cedms"},"schema":{"properties":{"new_owner":{"description":"New owner principal; must resolve to a user — a bare id/email (normalized to `user:\u003cid\u003e`) or an explicit `user:\u003cid\u003e` ref. Groups and wildcards are rejected (400).","type":"string"},"object":{"description":"The catalog to transfer — a bare catalog name (e.g. `cedms`) or a typed ref (e.g. `catalog:cedms`); any non-catalog object type is a 400 (only catalogs carry the `owner` relation).","type":"string"}},"required":["object","new_owner"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"object":"catalog:cedms","owner":"user:alice@edgentllc.com","previous":["user:bob@edgentllc.com"]},"schema":{"properties":{"object":{"description":"The normalized typed catalog ref that was transferred, e.g. `catalog:cedms`.","type":"string"},"owner":{"description":"The normalized new owner user ref, e.g. `user:alice@edgentllc.com`.","type":"string"},"previous":{"description":"The prior owner refs that were revoked (empty array `[]` when there were none, e.g. first assignment or a no-op transfer to the current owner).","items":{"type":"string"},"type":"array"}},"required":["object","owner","previous"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Transfers ownership of a catalog to a new principal by swapping the FGA `owner` tuple.","tags":["Admin"]}},"/api/v1/admin/catalog/resync":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/admin/documents/dedupe":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/admin/documents/purge":{"post":{"description":"Clears ALL processed-page state for the request's tenant: it truncates the microfiche gold table and deletes every ingest/preview/sheet object from the object store, then (best-effort) clears the ingest queue's batch/job history so the Batches list resets. This is the admin cleanup path for resetting the corpus after testing, and returns the counts removed.","responses":{"200":{"content":{"application/json":{"example":{"jobs_cleared":12,"objects_deleted":128,"table_truncated":true},"schema":{"properties":{"jobs_cleared":{"description":"Number of queue jobs cleared from the batch/job history (0 if no job queue is configured, since the clear is best-effort).","type":"integer"},"objects_deleted":{"description":"Number of ingest/preview/sheet objects deleted from the object store.","type":"integer"},"table_truncated":{"description":"Whether the gold (processed-pages) table was truncated.","type":"boolean"}},"required":["table_truncated","objects_deleted","jobs_cleared"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Clears ALL processed-page state for the request's tenant: it truncates the microfiche gold table and deletes every ingest/preview/sheet object from the object store, then (best-effort) clears the ingest queue's batch/job history so the Batches list resets.","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/admin/documents/reindex":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/admin/query-history/reindex":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/admin/sandbox/cleanup":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/admin/tier-sweep":{"post":{"description":"Runs one on-demand tiered-storage lifecycle sweep: it evaluates every object of the swept tenants against the configured lifecycle policy at the current time and re-tags (moves) each object whose target tier differs from its current tier, returning a SweepResult with the scanned/moved counts and per-object transitions. It always sweeps the shared tenant and optionally an additional tenant supplied in the body.","requestBody":{"content":{"application/json":{"example":{"tenant":"cedms"},"schema":{"properties":{"tenant":{"description":"Optional additional tenant id to sweep alongside the shared tenant; ignored if empty or equal to the shared tenant. An empty/whitespace body is valid and sweeps only the shared tenant.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"moved":3,"scanned":42,"transitions":[{"from":"hot","key":"ingest/cedms/pages/0007.tif","tenant_id":"shared","to":"warm"}]},"schema":{"properties":{"errors":{"description":"Per-object/per-tenant failures that did not abort the sweep (best-effort); omitted when empty (json omitempty).","items":{"type":"string"},"type":"array"},"moved":{"description":"Number of objects whose tier was changed.","type":"integer"},"scanned":{"description":"Number of objects evaluated across the swept tenants.","type":"integer"},"transitions":{"description":"Each move in encounter order: {tenant_id: string, key: string, from: string (storage tier), to: string (storage tier)}.","items":{"type":"object"},"type":"array"}},"required":["scanned","moved","transitions"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs one on-demand tiered-storage lifecycle sweep: it evaluates every object of the swept tenants against the configured lifecycle policy at the current time and re-tags (moves) each object whose target tier differs from its current tier, returning a SweepResult with the scanned/moved counts and per-object transitions.","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/admin/vectors/reconcile":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Admin"],"x-stability":"experimental"}},"/api/v1/ai/artifact":{"post":{"description":"Safe first slice of agentic create: generates an artifact SPEC (a pipeline definition or a dashboard JSON) grounded in the caller's readable estate and returns it for REVIEW ONLY. It performs no mutation — the response carries an apply gate that is shipped disabled, documenting that actually creating the projection/dashboard is a future write/admin-gated step.","requestBody":{"content":{"application/json":{"example":{"description":"daily rollup of epics added per project","kind":"pipeline"},"schema":{"properties":{"description":{"description":"Natural-language description of the artifact to generate; rejected (400) if blank.","type":"string"},"kind":{"description":"Artifact type; must be \"pipeline\" or \"dashboard\" (case-insensitive), else 400 'unsupported kind'.","type":"string"}},"required":["kind","description"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"apply":{"enabled":false,"reason":"spec review only — applying an artifact will be gated behind an explicit write/admin authorization check (see follow-up issues)."},"explanation":"Generated spec for review. Nothing was created.","kind":"pipeline","spec":{"description":"Daily count of epics added per project","materialization":"table","name":"daily_epics_rollup","sources":["orchestration.projected.epics_added"],"target":"orchestration.gold.daily_epics_rollup","transform_sql":"SELECT project_id, date_trunc('day', time) AS day, count(*) AS epics FROM orchestration.projected.epics_added GROUP BY 1, 2"}},"schema":{"properties":{"apply":{"description":"Apply gate {enabled:false, reason}: documents that applying the spec is deferred behind a future write/admin authz check.","type":"object"},"explanation":{"description":"Constant review notice: \"Generated spec for review. Nothing was created.\"","type":"string"},"kind":{"description":"Echo of the normalized artifact kind (pipeline|dashboard).","type":"string"},"spec":{"description":"The generated artifact spec as raw JSON (pipeline: name/description/sources/transform_sql/target/materialization; dashboard: name/widgets[]).","type":"object"}},"required":["kind","spec","explanation","apply"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Safe first slice of agentic create: generates an artifact SPEC (a pipeline definition or a dashboard JSON) grounded in the caller's readable estate and returns it for REVIEW ONLY.","tags":["AI"],"x-stability":"experimental"}},"/api/v1/ai/playground":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["AI"],"x-stability":"experimental"}},"/api/v1/ai/playground/history":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["AI"],"x-stability":"experimental"}},"/api/v1/ai/playground/history/{id}":{"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["AI"],"x-stability":"experimental"}},"/api/v1/ai/query":{"post":{"description":"Metadata-grounded Mnemo (the platform assistant): turns a natural-language question into ONE tenant-scoped read-only SELECT plus a suggested chart, grounded strictly in the tables the caller can `read` (same ReBAC gate as the catalog tree). The generated SQL is re-parsed and rejected if it references any table outside that readable allowlist, and it is NOT executed here — it is returned for the user to run through the ReBAC-gated /compute/sql path.","requestBody":{"content":{"application/json":{"example":{"catalog":"orchestration","question":"show me a sample of ten rows","schema":"projected"},"schema":{"properties":{"catalog":{"description":"Optional catalog filter to narrow the grounding snapshot to one catalog.","type":"string"},"question":{"description":"Natural-language question about the data; rejected (400) if blank.","type":"string"},"schema":{"description":"Optional schema filter to narrow the grounding snapshot to one schema.","type":"string"}},"required":["question"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"chart":{"type":"table"},"explanation":"This query selects the first ten rows from the orchestration.projected.architecture_approved view where the event_id is '2023-04-01T12:34:56.789Z'.","grounding":{"columns_dropped":0,"notes":["dropped 12 table(s) past the 12000-char budget"],"tables_dropped":12,"tables_included":29},"sql":"SELECT * FROM orchestration.projected.architecture_approved WHERE event_id = '2023-04-01T12:34:56.789Z' LIMIT 10"},"schema":{"properties":{"chart":{"description":"Suggested declarative chart spec: {type (bar|line|area|pie|table), x, y[], title}; unknown types normalize to \"table\".","type":"object"},"explanation":{"description":"One-or-two-sentence plain-language explanation of the query.","type":"string"},"grounding":{"description":"Honest account of context shaping: {tables_included, tables_dropped, columns_dropped, notes[]} showing when the estate exceeded the char budget.","type":"object"},"sql":{"description":"The generated single read-only SELECT (allowlist-guarded, never executed).","type":"string"}},"required":["sql","explanation","chart","grounding"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Metadata-grounded Mnemo (the platform assistant): turns a natural-language question into ONE tenant-scoped read-only SELECT plus a suggested chart, grounded strictly in the tables the caller can `read` (same ReBAC gate as the catalog tree).","tags":["AI"]}},"/api/v1/ai/visualize":{"post":{"description":"Post-query chart advisor: given a question and the ACTUAL result column names, asks the governed LLM for one declarative chart spec tailored to those columns. Returns only a chart spec — no data and no server-rendered image — and degrades gracefully to a table when the model output can't be parsed.","requestBody":{"content":{"application/json":{"example":{"columns":["day","total"],"question":"trend over time"},"schema":{"properties":{"columns":{"description":"The result column names to choose x/y from; rejected (400 'no columns') if empty.","items":{"type":"string"},"type":"array"},"question":{"description":"The natural-language question the result answers (used as context; not independently validated).","type":"string"}},"required":["columns"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"chart":{"type":"table"}},"schema":{"properties":{"chart":{"description":"Declarative chart spec {type (bar|line|area|pie|table), x, y[], title}; type normalized to \"table\" if unknown.","type":"object"}},"required":["chart"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Post-query chart advisor: given a question and the ACTUAL result column names, asks the governed LLM for one declarative chart spec tailored to those columns.","tags":["AI"]}},"/api/v1/alerts":{"get":{"description":"Lists all SQL alerts for the caller's tenant. It enumerates every JSON record under the tenant's alerts/ object-store prefix, unmarshals each into an alert, drops any that fail to decode or lack an id, and returns them sorted by created_at descending (newest first).","responses":{"200":{"content":{"application/json":{"example":{"alerts":[{"column":"n","created_at":"2026-07-04T16:17:44.153281661Z","created_by":"sofia.admin","id":"0e8e2912577ed046","last_checked":"2026-07-04T16:17:44.203296417Z","last_value":"188","name":"Flow Event Volume","op":"\u003e","sql":"SELECT count(*) AS n FROM orchestration.events.flow_runs","state":"ok","threshold":1000},{"column":"max_pop","created_at":"2026-07-04T16:15:28.074745329Z","created_by":"maya.analyst","id":"9de9bef6edc42a39","last_checked":"2026-07-04T16:15:28.110868297Z","last_value":"1450935791","name":"Extreme Population Growth","op":"\u003e","sql":"SELECT max(population) AS max_pop FROM demo.reference.population","state":"ok","threshold":2000000000},{"column":"max_temp","created_at":"2026-07-04T16:15:24.463285566Z","created_by":"sofia.admin","id":"490d568e9c516331","last_checked":"2026-07-04T16:15:25.128409432Z","last_value":"39.8","name":"High Max Temperature","op":"\u003e","sql":"SELECT max(tmax_c) AS max_temp FROM main.weather.observations","state":"triggered","threshold":35}]},"schema":{"properties":{"alerts":{"description":"Array of alert objects, newest first; each has id, name, sql, column, op, threshold, state, last_value, last_checked, created_by, created_at.","items":{"type":"object"},"type":"array"}},"required":["alerts"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists all SQL alerts for the caller's tenant.","tags":["Alerts"]},"post":{"description":"Creates a new SQL alert: a saved SQL query plus a threshold comparison on one result column. It trims and validates that name, sql, and column are non-empty and that op is one of the six supported comparison operators, then assigns a generated id, sets state to \"unknown\", stamps created_by (the request subject) and created_at, persists it to the tenant's alerts/ prefix, and returns the created alert with 201.","requestBody":{"content":{"application/json":{"example":{"column":"max_temp","name":"High Max Temperature","op":"\u003e","sql":"SELECT max(tmax_c) AS max_temp FROM main.weather.observations","threshold":35},"schema":{"properties":{"column":{"description":"Name of the result column (from the first row) to compare against the threshold; trimmed, must be non-empty.","type":"string"},"name":{"description":"Human-readable alert name; trimmed, must be non-empty.","type":"string"},"op":{"description":"Comparison operator; must be one of \u003e \u003e= \u003c \u003c= == != (validated against the alertOps map).","type":"string"},"sql":{"description":"SQL query to run on evaluation; trimmed, must be non-empty.","type":"string"},"threshold":{"description":"Numeric threshold the column value is compared to; defaults to 0 if omitted.","type":"number"}},"required":["name","sql","column","op"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"column":"max_temp","created_at":"2026-07-15T12:00:00.000000000Z","created_by":"demo-user","id":"a1b2c3d4e5f60718","name":"High Max Temperature","op":"\u003e","sql":"SELECT max(tmax_c) AS max_temp FROM main.weather.observations","state":"unknown","threshold":35},"schema":{"properties":{"column":{"description":"Result column name (trimmed).","type":"string"},"created_at":{"description":"RFC3339Nano creation timestamp.","type":"string"},"created_by":{"description":"Subject that created the alert (from subjectOf(r)).","type":"string"},"id":{"description":"Generated alert id (same generator as saved queries).","type":"string"},"last_checked":{"description":"RFC3339Nano timestamp of last evaluation; omitted until first evaluation.","type":"string"},"last_value":{"description":"Last evaluated column value; omitted (empty) until first evaluation.","type":"string"},"name":{"description":"Alert name as supplied (trimmed).","type":"string"},"op":{"description":"Comparison operator.","type":"string"},"sql":{"description":"SQL query as supplied (trimmed).","type":"string"},"state":{"description":"Alert state; always \"unknown\" on creation (later ok | triggered | error | unknown).","type":"string"},"threshold":{"description":"Numeric threshold.","type":"number"}},"required":["id","name","sql","column","op","threshold","state","created_by","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates a new SQL alert: a saved SQL query plus a threshold comparison on one result column.","tags":["Alerts"]}},"/api/v1/alerts/{id}":{"delete":{"description":"Deletes the alert with the given id from the caller's tenant by removing its JSON record (alerts/{id}.json) from the object store. Responds 204 No Content with an empty body on success.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the alert id to delete (trimmed).","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes the alert with the given id from the caller's tenant by removing its JSON record (alerts/{id}.json) from the object store.","tags":["Alerts"]},"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Alerts"],"x-stability":"experimental"}},"/api/v1/alerts/{id}/evaluate":{"post":{"description":"Evaluates a stored alert: loads it, runs its SQL against the tenant's compute engine, reads the target column from the first result row, parses it as a float, and compares it to the threshold using the alert's operator — setting state to \"triggered\" or \"ok\". Any failure (query error, no rows, missing column, non-numeric value) sets state to \"error\" with the reason in last_value. The updated alert (with new state, last_value, last_checked) is persisted and returned with 200.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the alert id to evaluate (trimmed).","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"column":"max_temp","created_at":"2026-07-04T16:15:24.463285566Z","created_by":"sofia.admin","id":"490d568e9c516331","last_checked":"2026-07-15T12:00:00.000000000Z","last_value":"39.8","name":"High Max Temperature","op":"\u003e","sql":"SELECT max(tmax_c) AS max_temp FROM main.weather.observations","state":"triggered","threshold":35},"schema":{"properties":{"column":{"description":"Result column compared.","type":"string"},"created_at":{"description":"Original creation timestamp.","type":"string"},"created_by":{"description":"Original creator subject.","type":"string"},"id":{"description":"Alert id.","type":"string"},"last_checked":{"description":"RFC3339Nano timestamp set to the moment of this evaluation.","type":"string"},"last_value":{"description":"On success, the parsed numeric column value formatted as a string; on failure, the error reason (query error text, \"query returned no rows\", missing-column or non-numeric message).","type":"string"},"name":{"description":"Alert name.","type":"string"},"op":{"description":"Comparison operator.","type":"string"},"sql":{"description":"SQL query that was run.","type":"string"},"state":{"description":"Post-evaluation state: triggered | ok | error.","type":"string"},"threshold":{"description":"Numeric threshold.","type":"number"}},"required":["id","name","sql","column","op","threshold","state","last_checked","created_by","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Evaluates a stored alert: loads it, runs its SQL against the tenant's compute engine, reads the target column from the first result row, parses it as a float, and compares it to the threshold using the alert's operator — setting state to \"triggered\" or \"ok\".","tags":["Alerts"]}},"/api/v1/ask/public/query":{"post":{"description":"The login-free Mnemo NL-\u003eSQL step: after the token gate, it grounds the governed chat model on ONLY the workspace's scoped tables (under the workspace tenant, since the caller has no identity), asks for one read-only SELECT, and double-guards the result against both the grounded allowlist and the scope ceiling. Returns the SQL for review (running is a separate call); if the model answers conversationally or the draft escapes scope, it returns a prose answer instead.","requestBody":{"content":{"application/json":{"example":{"history":[{"content":"show audit events","role":"user"},{"content":"...","role":"assistant"}],"question":"What operations happened most recently?"},"schema":{"properties":{"history":{"description":"Prior turns to refine a follow-up; each {role, content}. Only the last 6 are used.","items":{"type":"object"},"type":"array"},"question":{"description":"Natural-language question about the workspace data; trimmed, must be non-empty.","type":"string"},"t":{"description":"Token carrier query param (or Authorization: Bearer / X-Ask-Token header). One is required.","type":"string"}},"required":["question"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"chart":{"type":"table"},"explanation":"Lists the 20 most recent audit operations.","kind":"query","sql":"SELECT operation, resource, ts FROM main.audit.events ORDER BY ts DESC LIMIT 20","workspace":{"name":"Governance \u0026 Audit","scope":["main.audit.*"]}},"schema":{"properties":{"answer":{"description":"Conversational answer text (kind=prose only).","type":"string"},"chart":{"description":"Suggested chart spec {type,x,y,title}, type normalized to bar|line|area|pie|table (kind=query only).","type":"object"},"explanation":{"description":"One or two sentences describing the query (kind=query only).","type":"string"},"kind":{"description":"'query' when SQL was produced, or 'prose' for a conversational/blocked answer.","type":"string"},"sql":{"description":"The single read-only SELECT (kind=query only).","type":"string"},"workspace":{"description":"Public workspace descriptor {name, scope}.","type":"object"}},"required":["kind","workspace"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"The login-free Mnemo NL-\u003eSQL step: after the token gate, it grounds the governed chat model on ONLY the workspace's scoped tables (under the workspace tenant, since the caller has no identity), asks for one read-only SELECT, and double-guards the result against both the grounded allowlist and the scope ceiling.","tags":["Ask"],"x-stability":"experimental"}},"/api/v1/ask/public/run":{"post":{"description":"Executes a Mnemo-produced SQL statement behind the token gate. It RE-GUARDS the SQL against the workspace's grounded table set and scope ceiling (rejecting any out-of-scope or non-SELECT statement) BEFORE checking compute availability, tenantizes the FQNs under the workspace's tenant, and runs it on the distributed compute engine, returning the result rows.","requestBody":{"content":{"application/json":{"example":{"sql":"SELECT operation, resource, ts FROM main.audit.events ORDER BY ts DESC LIMIT 20"},"schema":{"properties":{"sql":{"description":"The read-only SELECT to execute; re-guarded against the workspace scope before running.","type":"string"},"t":{"description":"Token carrier query param (or Authorization: Bearer / X-Ask-Token header). One is required.","type":"string"}},"required":["sql"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"rows":[{"operation":"query","resource":"main.audit.events","ts":"2026-07-04T16:00:00Z"}]},"schema":{"properties":{"rows":{"description":"Result rows from the distributed query, each a column-\u003evalue map.","items":{"type":"object"},"type":"array"}},"required":["rows"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Executes a Mnemo-produced SQL statement behind the token gate.","tags":["Ask"],"x-stability":"experimental"}},"/api/v1/ask/public/visualize":{"post":{"description":"Token-gated advisory chart suggestion for a result set: given a question and the result column names, it asks the governed model for one chart spec and returns it normalized to the closed chart-type set. Same behavior as the authenticated /ai/visualize, behind the workspace token gate.","requestBody":{"content":{"application/json":{"example":{"columns":["ts","operation_count"],"question":"operations over time"},"schema":{"properties":{"columns":{"description":"The result column names to choose x/y from; must be non-empty.","items":{"type":"string"},"type":"array"},"question":{"description":"The question the result answers, used as context for the chart choice.","type":"string"},"t":{"description":"Token carrier query param (or Authorization: Bearer / X-Ask-Token header). One is required.","type":"string"}},"required":["columns"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"chart":{"title":"Operations over time","type":"line","x":"ts","y":["operation_count"]}},"schema":{"properties":{"chart":{"description":"Chart spec {type,x,y,title}; type normalized to bar|line|area|pie|table, defaults to table on parse failure.","type":"object"}},"required":["chart"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Token-gated advisory chart suggestion for a result set: given a question and the result column names, it asks the governed model for one chart spec and returns it normalized to the closed chart-type set.","tags":["Ask"],"x-stability":"experimental"}},"/api/v1/ask/public/workspace":{"get":{"description":"Token-gated public descriptor of a shared workspace, loaded by the login-free Ask page to render its name, description, scope, and starter questions. The token — via ?t=, Authorization: Bearer, or X-Ask-Token — is resolved by hash, liveness-checked, and its workspace loaded under the workspace's own tenant.","requestBody":{"content":{"application/json":{"schema":{"properties":{"t":{"description":"Query param carrier for the raw askw_ token (alternative to Authorization: Bearer or X-Ask-Token header). One carrier is required.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"description":"Scoped Q\u0026A over the platform audit trail.","examples":["What operations happened most recently?"],"name":"Governance \u0026 Audit","scope":["main.audit.*"]},"schema":{"properties":{"description":{"description":"Workspace description (omitted when empty).","type":"string"},"examples":{"description":"Starter questions (omitted when empty).","items":{"type":"string"},"type":"array"},"name":{"description":"Workspace name.","type":"string"},"scope":{"description":"Scope allowlist the token may query.","items":{"type":"string"},"type":"array"}},"required":["name","scope"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Token-gated public descriptor of a shared workspace, loaded by the login-free Ask page to render its name, description, scope, and starter questions.","tags":["Ask"]}},"/api/v1/ask/sessions":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Ask"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Ask"],"x-stability":"experimental"}},"/api/v1/ask/sessions/{id}":{"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Ask"],"x-stability":"experimental"},"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Ask"],"x-stability":"experimental"},"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Ask"],"x-stability":"experimental"}},"/api/v1/ask/sessions/{id}/ask":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Ask"],"x-stability":"experimental"}},"/api/v1/ask/suggestions":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Ask"],"x-stability":"experimental"}},"/api/v1/ask/workspaces":{"get":{"description":"Lists all Ask Workspaces owned by the caller's tenant, newest first (sorted by CreatedAt descending). Session-authed; only the requesting tenant's workspaces are returned.","responses":{"200":{"content":{"application/json":{"example":{"workspaces":[{"created_at":"2026-07-04T16:15:37.659330494Z","created_by":"sofia.admin","description":"Scoped Q\u0026A over the platform audit trail.","examples":["What operations happened most recently?","Which resources are queried most often?"],"id":"f6683716c415cc16","name":"Governance \u0026 Audit","scope":["main.audit.*"],"tenant_id":"demo","updated_at":"2026-07-04T16:15:37.659330494Z"},{"created_at":"2026-07-04T16:15:37.609030973Z","created_by":"raj.dataeng","description":"Scoped Q\u0026A over weather and sonar survey data.","examples":["What was the hottest day recorded?","How many PINGMapper survey runs have completed?"],"id":"13ce9df2b4183e9f","name":"Field Operations","scope":["main.weather.*","main.sonar.*"],"tenant_id":"demo","updated_at":"2026-07-04T16:15:37.609030973Z"}]},"schema":{"properties":{"workspaces":{"description":"Array of full Workspace objects (id, tenant_id, name, description, scope, examples, created_by, created_at, updated_at), newest first.","items":{"type":"object"},"type":"array"}},"required":["workspaces"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists all Ask Workspaces owned by the caller's tenant, newest first (sorted by CreatedAt descending).","tags":["Ask"]},"post":{"description":"Creates a named, scoped Ask Workspace under the caller's tenant. Before persisting, it normalizes the requested scope (3-segment FQNs/globs only) and runs the same ReBAC read gate as the AI query path against every table the scope would expose, so a workspace (and any token minted from it) can never widen access beyond its creator.","requestBody":{"content":{"application/json":{"example":{"description":"Scoped Q\u0026A over the platform audit trail.","examples":["What operations happened most recently?"],"name":"Governance \u0026 Audit","scope":["main.audit.*"]},"schema":{"properties":{"description":{"description":"Optional human description of the workspace.","type":"string"},"examples":{"description":"Optional starter questions shown on the public page; trimmed, blanks dropped, capped at 12.","items":{"type":"string"},"type":"array"},"name":{"description":"Workspace display name; trimmed, must be non-empty.","type":"string"},"scope":{"description":"Allowlist of catalog.schema.table FQNs or 3-segment globs (e.g. main.audit.*); the hard access ceiling. Must normalize to \u003e=1 well-formed entry.","items":{"type":"string"},"type":"array"}},"required":["name","scope"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-04T16:15:37.659330494Z","created_by":"sofia.admin","description":"Scoped Q\u0026A over the platform audit trail.","examples":["What operations happened most recently?"],"id":"f6683716c415cc16","name":"Governance \u0026 Audit","scope":["main.audit.*"],"tenant_id":"demo","updated_at":"2026-07-04T16:15:37.659330494Z"},"schema":{"properties":{"created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"created_by":{"description":"Subject that created the workspace.","type":"string"},"description":{"description":"Description (omitted when empty).","type":"string"},"examples":{"description":"Cleaned example questions (omitted when empty).","items":{"type":"string"},"type":"array"},"id":{"description":"Generated short hex workspace id.","type":"string"},"name":{"description":"Workspace name.","type":"string"},"scope":{"description":"Normalized scope allowlist.","items":{"type":"string"},"type":"array"},"tenant_id":{"description":"Owner tenant the workspace lives under.","type":"string"},"updated_at":{"description":"RFC3339 last-update timestamp.","type":"string"}},"required":["id","tenant_id","name","scope","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates a named, scoped Ask Workspace under the caller's tenant.","tags":["Ask"]}},"/api/v1/ask/workspaces/{id}":{"delete":{"description":"Deletes a workspace and first hard-deletes every token minted for it, so any shared link stops working immediately. Idempotent — deleting a missing workspace still returns 204.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path param: workspace id.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes a workspace and first hard-deletes every token minted for it, so any shared link stops working immediately.","tags":["Ask"]},"get":{"description":"Fetches one Ask Workspace by id within the caller's tenant, returning the full Workspace object including its scope allowlist and example questions.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path param: the workspace id.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-04T16:15:37.659330494Z","created_by":"sofia.admin","description":"Scoped Q\u0026A over the platform audit trail.","examples":["What operations happened most recently?","Which resources are queried most often?"],"id":"f6683716c415cc16","name":"Governance \u0026 Audit","scope":["main.audit.*"],"tenant_id":"demo","updated_at":"2026-07-04T16:15:37.659330494Z"},"schema":{"properties":{"created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"created_by":{"description":"Creator subject.","type":"string"},"description":{"description":"Description (omitted when empty).","type":"string"},"examples":{"description":"Example questions (omitted when empty).","items":{"type":"string"},"type":"array"},"id":{"description":"Workspace id.","type":"string"},"name":{"description":"Workspace name.","type":"string"},"scope":{"description":"Scope allowlist.","items":{"type":"string"},"type":"array"},"tenant_id":{"description":"Owner tenant.","type":"string"},"updated_at":{"description":"RFC3339 update timestamp.","type":"string"}},"required":["id","tenant_id","name","scope","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Fetches one Ask Workspace by id within the caller's tenant, returning the full Workspace object including its scope allowlist and example questions.","tags":["Ask"]},"patch":{"description":"Partially updates a workspace: only fields present in the body change. name/description/examples are edited in place; a supplied scope is re-normalized and re-authorized against the same ReBAC read gate as create before it replaces the existing scope. Returns the updated Workspace.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"description":"Updated audit Q\u0026A","scope":["main.audit.*","main.access.*"]},"schema":{"properties":{"description":{"description":"New description (pointer: only applied if present).","type":"string"},"examples":{"description":"Replacement example list; if present (non-null), replaces and is cleaned/capped at 12.","items":{"type":"string"},"type":"array"},"id":{"description":"Path param: workspace id.","type":"string"},"name":{"description":"New name (pointer: only applied if present); trimmed and must be non-empty if given.","type":"string"},"scope":{"description":"Replacement scope; if present (non-null), must normalize to \u003e=1 entry and the caller must be able to read every table it exposes.","items":{"type":"string"},"type":"array"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-04T16:15:37.659330494Z","created_by":"sofia.admin","description":"Updated audit Q\u0026A","examples":["What operations happened most recently?"],"id":"f6683716c415cc16","name":"Governance \u0026 Audit","scope":["main.audit.*","main.access.*"],"tenant_id":"demo","updated_at":"2026-07-05T10:00:00Z"},"schema":{"properties":{"created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"created_by":{"description":"Creator subject.","type":"string"},"description":{"description":"Description (possibly updated).","type":"string"},"examples":{"description":"Examples (possibly updated).","items":{"type":"string"},"type":"array"},"id":{"description":"Workspace id.","type":"string"},"name":{"description":"Name (possibly updated).","type":"string"},"scope":{"description":"Scope (possibly re-scoped).","items":{"type":"string"},"type":"array"},"tenant_id":{"description":"Owner tenant.","type":"string"},"updated_at":{"description":"RFC3339 update timestamp, bumped to now on save.","type":"string"}},"required":["id","tenant_id","name","scope","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Partially updates a workspace: only fields present in the body change.","tags":["Ask"]}},"/api/v1/ask/workspaces/{id}/tokens":{"get":{"description":"Lists the tokens minted for a workspace as safe views — hash (id), label, timestamps, and derived active flag — never the raw token. Filtered to the caller's tenant and this workspace, newest first.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path param: workspace id.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"tokens":[]},"schema":{"properties":{"tokens":{"description":"Array of token views. Each: id (token hash), label, expires_at, revoked_at, active, created_by, created_at. Empty array when none.","items":{"type":"object"},"type":"array"}},"required":["tokens"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists the tokens minted for a workspace as safe views — hash (id), label, timestamps, and derived active flag — never the raw token.","tags":["Ask"]},"post":{"description":"Mints a fresh high-entropy raw bearer token bound to the workspace, persisting only its sha256 hash. The raw token (askw_ prefix) is returned exactly once, alongside a relative share path and the safe token view; it can never be retrieved again.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"label":"public share link","ttlHours":720},"schema":{"properties":{"id":{"description":"Path param: workspace id the token is bound to.","type":"string"},"label":{"description":"Optional human label for the token; trimmed.","type":"string"},"ttlHours":{"description":"Optional lifetime in hours; \u003e0 sets ExpiresAt = now+ttlHours, otherwise the token never expires.","type":"integer"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"share_path":"/ask/w/askw_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0","token":"askw_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0","token_info":{"active":true,"created_at":"2026-07-04T10:00:00Z","created_by":"sofia.admin","expires_at":"2026-08-03T10:00:00Z","id":"9f2c...hash","label":"public share link"}},"schema":{"properties":{"share_path":{"description":"Relative shareable link: /ask/w/\u003crawtoken\u003e.","type":"string"},"token":{"description":"The raw askw_ bearer token — shown ONCE, never stored or returned again.","type":"string"},"token_info":{"description":"Safe token view: id (=hash), label, expires_at, revoked_at, active, created_by, created_at.","type":"object"}},"required":["token","share_path","token_info"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Mints a fresh high-entropy raw bearer token bound to the workspace, persisting only its sha256 hash.","tags":["Ask"]}},"/api/v1/ask/workspaces/{id}/tokens/{tokenId}":{"delete":{"description":"Revokes a single token (sets RevokedAt) so every public call using it fails from now on; the token record is retained (not deleted) as a soft revoke. tokenId is the token hash. Idempotent and ownership-checked: the token must belong to this tenant and workspace.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"tokenId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path param: workspace id the token must belong to.","type":"string"},"tokenId":{"description":"Path param: the token hash (the id from the token view), NOT the raw token.","type":"string"}},"required":["id","tokenId"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Revokes a single token (sets RevokedAt) so every public call using it fails from now on; the token record is retained (not deleted) as a soft revoke.","tags":["Ask"]}},"/api/v1/asset-events":{"post":{"description":"Publishes an asset-change event for a table and fans it out to every subscriber of that table whose event filter matches, creating one persisted notification per matching subscriber. It validates the table FQN and event type, requires the caller to hold the `unmask` (asset-management) relation on the table — not merely read — because publishing injects an operator-supplied message into other users' notification inboxes, truncates the message to 2000 characters, and returns how many notifications were delivered.","requestBody":{"content":{"application/json":{"example":{"event":"schema","message":"orders table schema changed: added column shipped_at","table":"main.analytics.orders"},"schema":{"properties":{"event":{"description":"Event type; must be one of: schema, deprecation, freshness, quality, ownership.","type":"string"},"message":{"description":"Free-text message stored on each delivered notification; silently truncated to 2000 characters.","type":"string"},"table":{"description":"Fully-qualified table identifier (catalog.schema.table) the event pertains to; must pass validTableFQN.","type":"string"}},"required":["table","event"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"delivered":3,"event":"schema","table":"main.analytics.orders"},"schema":{"properties":{"delivered":{"description":"Number of subscribers matched and for whom a notification was successfully persisted.","type":"integer"},"event":{"description":"Echoes the published event type.","type":"string"},"table":{"description":"Echoes the table FQN the event was published for.","type":"string"}},"required":["table","event","delivered"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Publishes an asset-change event for a table and fans it out to every subscriber of that table whose event filter matches, creating one persisted notification per matching subscriber.","tags":["Asset events"]}},"/api/v1/audit/log":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Audit"],"x-stability":"experimental"}},"/api/v1/audit/search/reindex":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Audit"],"x-stability":"experimental"}},"/api/v1/audit/status":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Audit"],"x-stability":"experimental"}},"/api/v1/audit/stream":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Audit"],"x-stability":"experimental"}},"/api/v1/audit/summary":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Audit"],"x-stability":"experimental"}},"/api/v1/audit/verify":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Audit"],"x-stability":"experimental"}},"/api/v1/authz/groups":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Authz"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Authz"],"x-stability":"experimental"}},"/api/v1/authz/groups/{group}/members":{"post":{"parameters":[{"in":"path","name":"group","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Authz"],"x-stability":"experimental"}},"/api/v1/authz/groups/{group}/members/{user}":{"delete":{"parameters":[{"in":"path","name":"group","required":true,"schema":{"type":"string"}},{"in":"path","name":"user","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Authz"],"x-stability":"experimental"}},"/api/v1/authz/tags":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Authz"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Authz"],"x-stability":"experimental"}},"/api/v1/authz/tags/{tag}/entitlements":{"delete":{"parameters":[{"in":"path","name":"tag","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Authz"],"x-stability":"experimental"},"get":{"parameters":[{"in":"path","name":"tag","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Authz"],"x-stability":"experimental"},"post":{"parameters":[{"in":"path","name":"tag","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Authz"],"x-stability":"experimental"}},"/api/v1/batches":{"get":{"description":"Returns every ingest batch's roll-up (per-state job counts plus created/updated/processed timestamps), newest first — the data behind the persistent Batches list. Results are backend-paged with a default page size of 48 and a hard cap of 200.","requestBody":{"content":{"application/json":{"schema":{"properties":{"limit":{"description":"Page size; defaults to 48, clamped to a max of 200, ignored if \u003c=0 or non-numeric.","type":"integer"},"offset":{"description":"Number of batches to skip for paging; defaults to 0, ignored if \u003c=0 or non-numeric.","type":"integer"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"batches":[],"limit":2,"offset":0,"total":0},"schema":{"properties":{"batches":{"description":"Array of BatchSummary; each has batch_id (string), stats (object: total/queued/running/done/failed ints), created_at (RFC3339), updated_at (RFC3339), and processed_at (RFC3339, omitted when zero via json omitzero).","items":{"type":"object"},"type":"array"},"limit":{"description":"Echoed (clamped) limit used for this page.","type":"integer"},"offset":{"description":"Echoed offset used for this page.","type":"integer"},"total":{"description":"Total number of batches across all pages.","type":"integer"}},"required":["batches","total","offset","limit"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns every ingest batch's roll-up (per-state job counts plus created/updated/processed timestamps), newest first — the data behind the persistent Batches list.","tags":["Batches"]}},"/api/v1/batches/contexts":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Batches"],"x-stability":"experimental"}},"/api/v1/batches/{id}":{"delete":{"description":"Removes ONE batch (a point-in-time ingestion): its gold rows (via a scoped rewrite of the insert-only gold table), its ingest masters and preview/box derivatives in object storage, and its queue jobs — so it disappears from the Batches list and the processed-pages table. It is the scoped, per-batch sibling of the admin purge; deleting an unknown batch is a no-op success with zero counts.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path param: the batch id to delete; a missing/blank id returns 400.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"batch_id":"demo-batch-1","jobs_deleted":12,"objects_deleted":24,"rows_deleted":12,"rows_kept":340},"schema":{"properties":{"batch_id":{"description":"Echoed batch id from the path.","type":"string"},"jobs_deleted":{"description":"Number of queue jobs removed for the batch; 0 if no JobQueue is configured or the job delete errored (best-effort, does not fail the request).","type":"integer"},"objects_deleted":{"description":"Number of object-store objects removed across three prefixes: ingest masters (ingest/\u003cbatch\u003e/), preview derivatives (preview/ingest/\u003cbatch\u003e/), and box derivatives (boxes/ingest/\u003cbatch\u003e/).","type":"integer"},"rows_deleted":{"description":"Number of gold rows removed for this batch.","type":"integer"},"rows_kept":{"description":"Number of gold rows retained (rewritten survivors not belonging to this batch).","type":"integer"}},"required":["batch_id","rows_deleted","rows_kept","objects_deleted","jobs_deleted"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Removes ONE batch (a point-in-time ingestion): its gold rows (via a scoped rewrite of the insert-only gold table), its ingest masters and preview/box derivatives in object storage, and its queue jobs — so it disappears from the Batches list and the processed-pages table.","tags":["Batches"]},"get":{"description":"Returns the per-state job counts for one batch (the progress roll-up). Note the handler name is handleBatchStatus; it reports live queued/running/done/failed tallies for the given batch id.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path param: the batch id to summarize.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"batch_id":"demo-batch-1","stats":{"done":0,"failed":0,"queued":0,"running":0,"total":0}},"schema":{"properties":{"batch_id":{"description":"Echoed batch id from the path.","type":"string"},"stats":{"description":"BatchStats: total, queued, running, done, failed (all ints).","type":"object"}},"required":["batch_id","stats"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the per-state job counts for one batch (the progress roll-up).","tags":["Batches"]}},"/api/v1/batches/{id}/context":{"put":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Batches"],"x-stability":"experimental"}},"/api/v1/batches/{id}/jobs":{"get":{"description":"Returns one batch's per-page ingest jobs — the progress console listing each page's identity, current state, and (when done) the derived extraction result. Always returns a jobs array (never null).","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path param: the batch id whose jobs to list.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"jobs":[]},"schema":{"properties":{"jobs":{"description":"Array of BatchJob; each has page_key (string), index (int), state (string), doc_type (string), disposition (string), confidence (float64), error (string), created_at (RFC3339), updated_at (RFC3339).","items":{"type":"object"},"type":"array"}},"required":["jobs"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns one batch's per-page ingest jobs — the progress console listing each page's identity, current state, and (when done) the derived extraction result.","tags":["Batches"]}},"/api/v1/batches/{id}/retry":{"post":{"description":"Requeues a batch's FAILED ingest jobs by flipping them back to 'queued' for the worker to re-claim; nothing is re-uploaded. With an optional body {\"key\":\"\u003cpage key\u003e\"} the retry is narrowed to a single document (row-level retry); an empty body retries every failed job in the batch.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"key":"ingest/demo-batch-1/page-0007.png"},"schema":{"properties":{"id":{"description":"Path param: the batch id to retry; a missing/blank id returns 400.","type":"string"},"key":{"description":"Optional page key to narrow the retry to one document; empty/omitted (and trimmed of whitespace) retries all failed jobs in the batch. Body decode errors are ignored (body is optional).","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"requeued":3},"schema":{"properties":{"requeued":{"description":"Number of failed jobs flipped back to queued.","type":"integer"}},"required":["requeued"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Requeues a batch's FAILED ingest jobs by flipping them back to 'queued' for the worker to re-claim; nothing is re-uploaded.","tags":["Batches"]}},"/api/v1/batches/{id}/verify":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Batches"],"x-stability":"experimental"},"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Batches"],"x-stability":"experimental"}},"/api/v1/billing/rate-card":{"get":{"description":"Returns the platform's active global rate card: the price list that turns metered work into dollars — USD per VFU (VForce Unit, the compute credit), USD per GB-month of storage, USD per 1k LLM tokens, and the minimum billable seconds floor applied to any run. The handler ignores the request entirely and always emits billing.DefaultRateCard(), so the response is a fixed constant today (per-tenant contract cards are a future slice).","responses":{"200":{"content":{"application/json":{"example":{"min_billable_seconds":60,"usd_per_1k_tokens":0.015,"usd_per_gb_month":0.023,"usd_per_vfu":0.1},"schema":{"properties":{"min_billable_seconds":{"description":"Minimum billable window in seconds charged for any run; default 60.","type":"integer"},"usd_per_1k_tokens":{"description":"Dollars per 1,000 LLM tokens; default 0.015.","type":"number"},"usd_per_gb_month":{"description":"Dollars per GB-month of storage; default 0.023 (~S3 standard).","type":"number"},"usd_per_vfu":{"description":"Dollars charged per VFU (compute credit); default 0.10.","type":"number"}},"required":["usd_per_vfu","usd_per_gb_month","usd_per_1k_tokens","min_billable_seconds"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the platform's active global rate card: the price list that turns metered work into dollars — USD per VFU (VForce Unit, the compute credit), USD per GB-month of storage, USD per 1k LLM tokens, and the minimum billable seconds floor applied to any run.","tags":["Billing"]}},"/api/v1/billing/settings":{"get":{"description":"Returns the calling tenant's billing + placement settings (epic #190 BYOC): its deployment tier, billing mode, and optional placement/license-tier. When no settings are stored for the tenant (or the object store is unconfigured, or the stored blob is malformed / has an empty billing_mode) it falls back to the hosted default (shared tier, hosted-usage mode) so existing tenants behave unchanged. Always returns 200 — an unset tenant is not an error.","responses":{"200":{"content":{"application/json":{"example":{"billing_mode":"hosted-usage","deployment_tier":"shared"},"schema":{"properties":{"billing_mode":{"description":"How the tenant is charged: hosted-usage (meter VFU/storage) | byoc-license (their cloud bills them, we bill a license). Default hosted-usage.","type":"string"},"deployment_tier":{"description":"Where the tenant's compute runs: shared | dedicated | isolated. Default shared.","type":"string"},"license_tier":{"description":"Flat-plan name for byoc-license tenants (free-form); omitted when empty.","type":"string"},"placement":{"description":"Opaque cluster/cloud reference (e.g. \"eks:us-east-1\", \"tenant-byoc:acme-prod\"); omitted when empty.","type":"string"}},"required":["deployment_tier","billing_mode"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the calling tenant's billing + placement settings (epic #190 BYOC): its deployment tier, billing mode, and optional placement/license-tier.","tags":["Billing"]},"put":{"description":"Validates and persists the calling tenant's billing + placement settings, replacing any prior value. The body is normalized (tier and mode lower-cased and trimmed, placement/license_tier trimmed) and validated: tier must be shared|dedicated|isolated, mode must be hosted-usage|byoc-license, and a shared tenant may not be byoc-license (shared infra is always ours). On success it writes the cleaned object to the object store under billing/settings.json and returns the stored, normalized settings.","requestBody":{"content":{"application/json":{"example":{"billing_mode":"byoc-license","deployment_tier":"isolated","license_tier":"enterprise","placement":"eks:us-east-1"},"schema":{"properties":{"billing_mode":{"description":"hosted-usage | byoc-license (case-insensitive, trimmed). Invalid values 400.","type":"string"},"deployment_tier":{"description":"shared | dedicated | isolated (case-insensitive, trimmed). Invalid values 400.","type":"string"},"license_tier":{"description":"Flat-plan name for byoc-license tenants; trimmed. Not otherwise validated.","type":"string"},"placement":{"description":"Opaque cluster/cloud reference; trimmed. Not otherwise validated.","type":"string"}},"required":["deployment_tier","billing_mode"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"billing_mode":"byoc-license","deployment_tier":"isolated","license_tier":"enterprise","placement":"eks:us-east-1"},"schema":{"properties":{"billing_mode":{"description":"Stored, normalized billing mode.","type":"string"},"deployment_tier":{"description":"Stored, normalized deployment tier.","type":"string"},"license_tier":{"description":"Stored license tier; omitted when empty.","type":"string"},"placement":{"description":"Stored placement; omitted when empty.","type":"string"}},"required":["deployment_tier","billing_mode"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Validates and persists the calling tenant's billing + placement settings, replacing any prior value.","tags":["Billing"]}},"/api/v1/branding":{"get":{"description":"Returns the runtime theme/branding configuration that the web app fetches on load to apply the product name and CSS custom-property token overrides. Defaults to the VForce brand when no deployment override is wired: an empty ProductName falls back to \"VForce Lakehouse\" and a nil Tokens map is normalized to an empty object before serialization.","responses":{"200":{"content":{"application/json":{"example":{"product_name":"VForce Lakehouse","tokens":{}},"schema":{"properties":{"product_name":{"description":"Product/brand display name; defaults to \"VForce Lakehouse\" when unset.","type":"string"},"tokens":{"description":"CSS custom-property overrides keyed by property name without the leading \"--\" (e.g. \"vf-brand\"), applied on :root over the baked-in VForce defaults; empty object when none configured.","type":"string"}},"required":["product_name","tokens"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the runtime theme/branding configuration that the web app fetches on load to apply the product name and CSS custom-property token overrides.","tags":["Branding"]}},"/api/v1/catalog/catalogs/{catalog}":{"delete":{"description":"Deletes a catalog: gathers its schemas (schemasOfCatalog across seed/live/registry), deactivates it first (removes from the registry and suppresses it from the tree so it is immediately invisible/inaccessible), then cascade-drops each schema's namespace on the distributed engine in the background. Returns 202 immediately. Admin-gated (authorizeDelete), tenant-scoped, idempotent, reversible via re-create.","parameters":[{"in":"path","name":"catalog","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"catalog":{"description":"Path segment: catalog name.","type":"string"}},"required":["catalog"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"deleted":"analytics","status":"deleting"},"schema":{"properties":{"deleted":{"description":"The catalog name that was deactivated.","type":"string"},"status":{"description":"Always 'deleting' — per-schema CASCADE drops run in the background.","type":"string"}},"required":["deleted","status"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes a catalog: gathers its schemas (schemasOfCatalog across seed/live/registry), deactivates it first (removes from the registry and suppresses it from the tree so it is immediately invisible/inaccessible), then cascade-drops each schema's namespace on the distributed engine in the background.","tags":["Catalog"]}},"/api/v1/catalog/collections":{"get":{"description":"Returns the tenant's batch → collection-FQN assignment map: for each ingested batch id, the documents-object FQN (catalog.schema.object) it was assigned to at ingest time. Powers the documents-object browser and Documents-page collection scoping without touching the gold-table schema.","requestBody":{"content":{"application/json":{"schema":{"properties":{"(none)":{"description":"No params or body; tenant-scoped.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"collections":{"02327d94fbfd8d848f56707ff25d5936":"cedms.microfiche.gov-docs","0256aab34c2781838f0a2f701178c5c3":"cedms.microfiche.gov-docs"}},"schema":{"properties":{"collections":{"description":"batchID → collection FQN (catalog.schema.object). Empty object when none or store unwired.","type":"string"}},"required":["collections"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the tenant's batch → collection-FQN assignment map: for each ingested batch id, the documents-object FQN (catalog.schema.object) it was assigned to at ingest time.","tags":["Catalog"]}},"/api/v1/catalog/erd":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Catalog"],"x-stability":"experimental"}},"/api/v1/catalog/graph":{"get":{"description":"Returns the catalog DAG snapshot — catalog → schema → table → column nodes joined by 'contains' (and view 'depends_on') edges — that backs SQL intellisense and query pre-flight. Built from catalog metadata only (never touches the compute/warehouse plane); served from a TTL cache unless ?refresh=1 forces a rebuild.","requestBody":{"content":{"application/json":{"schema":{"properties":{"refresh":{"description":"Query param; '1' bypasses the graph cache and forces a live rebuild. Any other value serves the cached snapshot.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"builtAt":"0001-01-01T00:00:00Z","edges":[{"from":"main","rel":"contains","to":"main.audit"},{"from":"main.audit","rel":"contains","to":"main.audit.usage"},{"from":"main.audit.usage","rel":"contains","to":"main.audit.usage.ts"}],"nodes":[{"id":"main","kind":"catalog","name":"main"},{"id":"main.audit","kind":"schema","name":"audit"},{"id":"main.audit.usage","kind":"table","name":"usage"},{"id":"main.audit.usage.ts","kind":"column","name":"ts","type":"string"}]},"schema":{"properties":{"builtAt":{"description":"RFC3339 build timestamp of the snapshot (zero value 0001-01-01T00:00:00Z when built directly from Catalog.Graph rather than a warmed CatalogGraph cache).","type":"string"},"edges":{"description":"Parent→child edges, each {from, to, rel}.","items":{"type":"object"},"type":"array"},"edges[].from":{"description":"Parent node id.","type":"string"},"edges[].rel":{"description":"contains | depends_on.","type":"string"},"edges[].to":{"description":"Child node id.","type":"string"},"nodes":{"description":"DAG nodes, each {id, kind, name, type?}.","items":{"type":"object"},"type":"array"},"nodes[].id":{"description":"Fully-qualified path, e.g. 'main.audit.usage' or a column 'main.audit.usage.ts'.","type":"string"},"nodes[].kind":{"description":"catalog | schema | table | column.","type":"string"},"nodes[].name":{"description":"Leaf segment of the id.","type":"string"},"nodes[].type":{"description":"Logical column type; present (omitempty) on column nodes when known.","type":"string"}},"required":["nodes","nodes[].id","nodes[].kind","nodes[].name","edges","edges[].from","edges[].to","edges[].rel","builtAt"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the catalog DAG snapshot — catalog → schema → table → column nodes joined by 'contains' (and view 'depends_on') edges — that backs SQL intellisense and query pre-flight.","tags":["Catalog"]}},"/api/v1/catalog/namespaces":{"get":{"description":"Returns the tenant's user-created namespace registry — the catalogs/schemas/objects a user declared (before ingest, or as derived views) — sorted by name (sortRegistry). This is the raw registry, distinct from GET /tree which merges it with live/seed namespaces and ReBAC-filters.","requestBody":{"content":{"application/json":{"schema":{"properties":{"(none)":{"description":"No params or body; tenant-scoped.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"catalogs":[{"name":"demo","schemas":[{"name":"bronze","objects":[{"name":"gapminder","type":"table"}]},{"name":"gold","objects":[{"name":"gapminder_by_continent","type":"table"},{"name":"notebook_cell_chaining_write","type":"table"}]},{"name":"probe","objects":[]}]},{"name":"forensics","schemas":[{"name":"tattoos","objects":[{"name":"gallery","type":"table"}]}]}]},"schema":{"properties":{"catalogs":{"description":"Registered catalogs, each {name, schemas}.","items":{"type":"object"},"type":"array"},"catalogs[].name":{"description":"Catalog name.","type":"string"},"catalogs[].schemas":{"description":"Schemas, each {name, objects}.","items":{"type":"object"},"type":"array"},"catalogs[].schemas[].name":{"description":"Schema name.","type":"string"},"catalogs[].schemas[].objects":{"description":"Declared objects, each {name, type, columns?, source_type?, array?, parent?}.","items":{"type":"object"},"type":"array"},"catalogs[].schemas[].objects[].columns":{"description":"{name,type} view columns (omitempty); present only on derived-view objects.","items":{"type":"object"},"type":"array"},"catalogs[].schemas[].objects[].name":{"description":"Object name.","type":"string"},"catalogs[].schemas[].objects[].type":{"description":"table | documents | view.","type":"string"}},"required":["catalogs","catalogs[].name","catalogs[].schemas","catalogs[].schemas[].name","catalogs[].schemas[].objects","catalogs[].schemas[].objects[].name","catalogs[].schemas[].objects[].type"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the tenant's user-created namespace registry — the catalogs/schemas/objects a user declared (before ingest, or as derived views) — sorted by name (sortRegistry).","tags":["Catalog"]},"post":{"description":"Creates a catalog, schema, or object in the tenant's namespace registry. Creating a schema/object auto-creates its parent catalog/schema so a full path can be declared in one call; idempotent (re-creating an object retypes, re-creating a catalog/schema is a no-op). Also un-suppresses a previously-deleted seed namespace of the same path, making a prior delete reversible.","requestBody":{"content":{"application/json":{"example":{"catalog":"analytics","level":"object","name":"revenue","schema":"gold","type":"table"},"schema":{"properties":{"catalog":{"description":"Parent catalog; required (non-empty) for level=schema and level=object.","type":"string"},"level":{"description":"One of catalog | schema | object; anything else ⇒ 400.","type":"string"},"name":{"description":"Leaf name to create (validated non-empty at every level).","type":"string"},"schema":{"description":"Parent schema; required (non-empty) for level=object.","type":"string"},"type":{"description":"Object type for level=object: 'table' or (default) 'documents'.","type":"string"}},"required":["level","name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"catalogs":[{"name":"analytics","schemas":[{"name":"gold","objects":[{"name":"revenue","type":"table"}]}]}]},"schema":{"properties":{"catalogs":{"description":"The full updated registry (same shape as GET /namespaces), returned with 201 Created (not re-sorted on this path).","items":{"type":"object"},"type":"array"}},"required":["catalogs"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates a catalog, schema, or object in the tenant's namespace registry.","tags":["Catalog"]}},"/api/v1/catalog/namespaces/delete":{"post":{"description":"Removes a catalog, schema, or object from the tenant's namespace registry (declaration only). Does NOT drop data or suppress seed/live namespaces — namespaces that still have physical data keep appearing in the tree, sourced from the data itself. Use DELETE /catalog/tables|schemas|catalogs to actually drop data.","requestBody":{"content":{"application/json":{"example":{"catalog":"analytics","level":"object","name":"revenue","schema":"gold"},"schema":{"properties":{"catalog":{"description":"Parent catalog; used for level=schema and level=object lookup.","type":"string"},"level":{"description":"catalog | schema | object; anything else ⇒ 400.","type":"string"},"name":{"description":"Leaf name to remove from the registry (used in the case-insensitive filter; not separately validated non-empty).","type":"string"},"schema":{"description":"Parent schema; used for level=object lookup.","type":"string"}},"required":["level","name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"catalogs":[{"name":"analytics","schemas":[{"name":"gold","objects":[]}]}]},"schema":{"properties":{"catalogs":{"description":"The full updated registry after removal (same shape as GET /namespaces), 200 OK.","items":{"type":"object"},"type":"array"}},"required":["catalogs"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Removes a catalog, schema, or object from the tenant's namespace registry (declaration only).","tags":["Catalog"]}},"/api/v1/catalog/objects/tag":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Catalog"],"x-stability":"experimental"}},"/api/v1/catalog/objects/tags":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Catalog"],"x-stability":"experimental"}},"/api/v1/catalog/objects/untag":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Catalog"],"x-stability":"experimental"}},"/api/v1/catalog/permissions":{"get":{"description":"Lists the direct ReBAC grants (principal + relation) on a catalog object plus the relations assignable on that object type — the data behind the Manage console's Permissions panel. Admin-gated on the object's admin relation (authorizeAuthzAdmin → authorizeDelete).","requestBody":{"content":{"application/json":{"schema":{"properties":{"object":{"description":"Query param; a bare id (0 dots=catalog, 1=schema, 2=table) or a typed ref (catalog:x / schema:a.b / table:a.b.c). Missing/malformed ⇒ 400.","type":"string"}},"required":["object"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"grants":[{"relation":"viewer","user":"user:maya.analyst"},{"relation":"owner","user":"user:maya.analyst"}],"object":"catalog:demo","relations":["reader","viewer","owner","admin"]},"schema":{"properties":{"grants":{"description":"Direct grants, each {user, relation}; [] when none.","items":{"type":"object"},"type":"array"},"grants[].relation":{"description":"Relation held, e.g. reader/viewer/owner/admin.","type":"string"},"grants[].user":{"description":"Principal, e.g. 'user:maya.analyst'.","type":"string"},"object":{"description":"Normalized typed FGA object ref, e.g. 'catalog:demo'.","type":"string"},"relations":{"description":"Assignable relations for this object type: catalog=[reader,viewer,owner,admin]; schema/table=[reader,viewer,admin] (no owner).","items":{"type":"string"},"type":"array"}},"required":["object","grants","grants[].user","grants[].relation","relations"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists the direct ReBAC grants (principal + relation) on a catalog object plus the relations assignable on that object type — the data behind the Manage console's Permissions panel.","tags":["Catalog"]}},"/api/v1/catalog/schemas/{catalog}/{schema}":{"delete":{"description":"Deletes a schema: deactivates it first (removes it from the registry and adds it to the tenant's suppressed-namespaces set so it is immediately invisible and reads/queries against it are rejected), then starts an async DROP NAMESPACE … CASCADE on the distributed engine to physically drop its tables/views. Returns 202 immediately. Admin-gated (authorizeDelete), tenant-scoped, idempotent, and reversible (re-creating the schema un-suppresses it).","parameters":[{"in":"path","name":"catalog","required":true,"schema":{"type":"string"}},{"in":"path","name":"schema","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"catalog":{"description":"Path segment: catalog name.","type":"string"},"schema":{"description":"Path segment: schema name.","type":"string"}},"required":["catalog","schema"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"deleted":"analytics.gold","status":"deleting"},"schema":{"properties":{"deleted":{"description":"The 'catalog.schema' that was deactivated.","type":"string"},"status":{"description":"Always 'deleting' — the physical CASCADE drop runs in the background.","type":"string"}},"required":["deleted","status"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes a schema: deactivates it first (removes it from the registry and adds it to the tenant's suppressed-namespaces set so it is immediately invisible and reads/queries against it are rejected), then starts an async DROP NAMESPACE … CASCADE on the distributed engine to physically drop its tables/views.","tags":["Catalog"]}},"/api/v1/catalog/search":{"get":{"description":"Ranks catalog tables/views against a free-text ?q= over table name, columns, tags (from the PII classifier), and description, returning scored hits — 'where's the patient data?' in one query. Reads the cached catalog graph snapshot (not a live per-namespace re-enumeration) plus the registry, and ReBAC-filters so only tables the subject can read are searched. An empty query returns no hits.","requestBody":{"content":{"application/json":{"schema":{"properties":{"q":{"description":"Query param; case-insensitive substring matched against name/columns/tags and as whitespace tokens against description. Empty/whitespace ⇒ {\"hits\":[]}.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"hits":[{"columns":[],"description":"","fqn":"demo.bronze.gapminder","name":"gapminder","score":60,"tags":null},{"columns":[],"description":"","fqn":"demo.silver.gapminder","name":"gapminder","score":60,"tags":null},{"columns":[],"description":"","fqn":"demo.gold.gapminder_by_continent","name":"gapminder_by_continent","score":28,"tags":null}]},"schema":{"properties":{"hits":{"description":"Ranked results, highest score first.","items":{"type":"object"},"type":"array"},"hits[].columns":{"description":"Column names considered in scoring; key always present ([] when the graph snapshot carried no column child-nodes).","items":{"type":"string"},"type":"array"},"hits[].description":{"description":"Table description; key always present, empty string in current data.","type":"string"},"hits[].fqn":{"description":"catalog.schema.table.","type":"string"},"hits[].name":{"description":"Table leaf name.","type":"string"},"hits[].score":{"description":"Match score; exact name match 60, name substring 20+coverage, tag 8, column 5, description token 2.","type":"integer"},"hits[].tags":{"description":"Flat object/column tag strings (e.g. 'pii:ssn'); key always present, null when none.","items":{"type":"string"},"type":"array"}},"required":["hits","hits[].fqn","hits[].name","hits[].columns","hits[].tags","hits[].description","hits[].score"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Ranks catalog tables/views against a free-text ?q= over table name, columns, tags (from the PII classifier), and description, returning scored hits — 'where's the patient data?' in one query.","tags":["Catalog"]}},"/api/v1/catalog/tables/{catalog}/{schema}/{table}":{"delete":{"description":"Drops a catalog table or view AND its data: an Iceberg table via Compute.DropTable, or (for a registry-declared derived view) a physical Spark DROP VIEW IF EXISTS on the distributed engine — the Databricks DROP TABLE/VIEW equivalent. Also removes the object's declaration from the namespace registry so it leaves the tree. Admin-gated server-side (authorizeDelete), tenant-scoped, idempotent.","parameters":[{"in":"path","name":"catalog","required":true,"schema":{"type":"string"}},{"in":"path","name":"schema","required":true,"schema":{"type":"string"}},{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"catalog":{"description":"Path segment: catalog name.","type":"string"},"schema":{"description":"Path segment: schema name.","type":"string"},"table":{"description":"Path segment: table/view name.","type":"string"}},"required":["catalog","schema","table"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"dropped":"analytics.gold.revenue","warnings":null},"schema":{"properties":{"dropped":{"description":"The FQN (catalog.schema.table) that was dropped.","type":"string"},"warnings":{"description":"Non-fatal warnings (e.g. a view's physical drop could not be confirmed); key always present, serializes null on a clean drop.","items":{"type":"string"},"type":"array"}},"required":["dropped","warnings"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Drops a catalog table or view AND its data: an Iceberg table via Compute.DropTable, or (for a registry-declared derived view) a physical Spark DROP VIEW IF EXISTS on the distributed engine — the Databricks DROP TABLE/VIEW equivalent.","tags":["Catalog"]}},"/api/v1/catalog/tags":{"get":{"description":"Returns the human-curated, object-level free-form tags ('gold', 'finance', 'deprecated') on a catalog/schema/table — Unity-Catalog-tags parity, rendered as chips on the catalog detail panel. Distinct from PII column tags. Read-gated: anyone who may read the object may see its tags.","requestBody":{"content":{"application/json":{"schema":{"properties":{"object":{"description":"Query param; bare id (0/1/2 dots) or typed ref (catalog:/schema:/table:). Missing/malformed ⇒ 400.","type":"string"}},"required":["object"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"object":"table:demo.gold.gapminder_by_continent","tags":[]},"schema":{"properties":{"object":{"description":"Normalized typed FGA object ref.","type":"string"},"tags":{"description":"Curated tag strings; [] when unset.","items":{"type":"string"},"type":"array"},"updated_at":{"description":"RFC3339 last-write time (omitempty; omitted when never set).","type":"string"},"updated_by":{"description":"Subject who last wrote the tags (omitempty; omitted when never set).","type":"string"}},"required":["object","tags"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the human-curated, object-level free-form tags ('gold', 'finance', 'deprecated') on a catalog/schema/table — Unity-Catalog-tags parity, rendered as chips on the catalog detail panel.","tags":["Catalog"]},"put":{"description":"Replaces (full overwrite, not merge) the free-form tag set on a catalog object. Tags are trimmed, rejected if empty or \u003e64 chars, de-duplicated case-insensitively (first spelling wins), and capped at 32 per object. Admin-gated on the object (authorizeAuthzAdmin) — the server re-checks regardless of the UI hiding the editor.","requestBody":{"content":{"application/json":{"example":{"tags":["gold","finance"]},"schema":{"properties":{"object":{"description":"Query param (NOT body); bare id or typed ref identifying the object. Missing/malformed ⇒ 400.","type":"string"},"tags":{"description":"JSON body field; the complete replacement tag list. Any empty-string entry ⇒ 400; a nil/omitted list clears the tag set.","items":{"type":"string"},"type":"array"}},"required":["object","tags"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"object":"table:demo.gold.gapminder_by_continent","tags":["gold","finance"],"updated_at":"2026-07-15T00:00:00Z","updated_by":"user:anonymous"},"schema":{"properties":{"object":{"description":"Normalized typed FGA object ref.","type":"string"},"tags":{"description":"The normalized, de-duplicated tag set now stored.","items":{"type":"string"},"type":"array"},"updated_at":{"description":"RFC3339 timestamp of this write (omitempty).","type":"string"},"updated_by":{"description":"Subject that performed this write (omitempty; subjectOf(r)).","type":"string"}},"required":["object","tags"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Replaces (full overwrite, not merge) the free-form tag set on a catalog object.","tags":["Catalog"]}},"/api/v1/catalog/tree":{"get":{"description":"Returns the full catalogs → schemas → tables → columns tree behind the Catalog 'Browse your data' view in a single call. It unions the compiled seed namespaces, the live catalog backend's enumerable namespaces, and the user-created namespace registry (so empty catalogs/schemas and declared 'documents'/'view' objects also appear), subtracts the tenant's suppressed (deleted) namespaces, and ReBAC-filters so tables the subject cannot read are omitted.","requestBody":{"content":{"application/json":{"schema":{"properties":{"(none)":{"description":"No query params or body; tenant is derived from the request host/context.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"catalogs":[{"can_delete":true,"name":"demo","schemas":[{"can_delete":true,"name":"bronze","tables":[{"can_delete":true,"columns":null,"name":"gapminder","type":"table"}]},{"can_delete":true,"name":"probe","tables":null}]},{"can_delete":true,"name":"isd","schemas":[{"can_delete":true,"name":"events","tables":[{"can_delete":true,"columns":[],"name":"courses","type":"table"}]},{"can_delete":true,"name":"projected","tables":[{"can_delete":true,"columns":[{"name":"event_id","type":"string"},{"name":"title","type":"string"}],"derived":true,"name":"course_authored","source_type":"com.ltm.workforce.isd.course.authored","type":"view"}]}]}],"configured":true},"schema":{"properties":{"catalogs":{"description":"Alphabetically ordered catalogs, each {name, schemas, can_delete}.","items":{"type":"object"},"type":"array"},"catalogs[].can_delete":{"description":"Whether the subject holds delete/admin authority (drives the UI menu only; the delete endpoint re-checks).","type":"boolean"},"catalogs[].name":{"description":"Catalog name.","type":"string"},"catalogs[].schemas":{"description":"Schemas under the catalog, each {name, tables, can_delete}.","items":{"type":"object"},"type":"array"},"catalogs[].schemas[].can_delete":{"description":"Subject's delete authority on the schema.","type":"boolean"},"catalogs[].schemas[].name":{"description":"Schema name.","type":"string"},"catalogs[].schemas[].tables":{"description":"Tables/views under the schema; key always present but serializes as null for a schema with no tables (e.g. an empty registry schema).","items":{"type":"object"},"type":"array"},"catalogs[].schemas[].tables[].array":{"description":"Shredded array field for child views only (omitempty).","type":"string"},"catalogs[].schemas[].tables[].can_delete":{"description":"Subject's delete authority on the table.","type":"boolean"},"catalogs[].schemas[].tables[].columns":{"description":"Column {name,type} list; key is always present. null for registry-sourced tables and live tables whose columns weren't enumerated, [] for enumerated seed/live tables, populated for derived views.","items":{"type":"object"},"type":"array"},"catalogs[].schemas[].tables[].derived":{"description":"Set only for projected views (omitempty); marks event-projection derived views.","type":"boolean"},"catalogs[].schemas[].tables[].name":{"description":"Table/object leaf name.","type":"string"},"catalogs[].schemas[].tables[].parent":{"description":"Parent view name for child views only (omitempty).","type":"string"},"catalogs[].schemas[].tables[].source_type":{"description":"CloudEvent type a derived view projects (views only; omitempty).","type":"string"},"catalogs[].schemas[].tables[].type":{"description":"table | documents | view.","type":"string"},"configured":{"description":"True when at least one visible catalog exists after filtering; false when the catalog backend is unwired or nothing is visible.","type":"boolean"}},"required":["configured","catalogs","catalogs[].name","catalogs[].can_delete","catalogs[].schemas","catalogs[].schemas[].name","catalogs[].schemas[].can_delete","catalogs[].schemas[].tables","catalogs[].schemas[].tables[].name","catalogs[].schemas[].tables[].type","catalogs[].schemas[].tables[].columns","catalogs[].schemas[].tables[].can_delete"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the full catalogs → schemas → tables → columns tree behind the Catalog 'Browse your data' view in a single call.","tags":["Catalog"]}},"/api/v1/catalog/{catalog}/{schema}/tables":{"get":{"description":"Lists the fully-qualified table names in a single catalog.schema namespace via the catalog backend's ListTables. ReBAC object-level filter: when enforcement is on, tables the subject cannot read are dropped from the list (rather than 403'ing the whole call); when off, all pass through.","parameters":[{"in":"path","name":"catalog","required":true,"schema":{"type":"string"}},{"in":"path","name":"schema","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"catalog":{"description":"Path segment: catalog name.","type":"string"},"schema":{"description":"Path segment: schema name.","type":"string"}},"required":["catalog","schema"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"tables":[]},"schema":{"properties":{"tables":{"description":"Fully-qualified table names (catalog.schema.table) the subject may read; [] when the namespace can't be enumerated by the backend.","items":{"type":"string"},"type":"array"}},"required":["tables"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists the fully-qualified table names in a single catalog.schema namespace via the catalog backend's ListTables.","tags":["Catalog"]}},"/api/v1/catalog/{fqn}/lineage":{"get":{"description":"Returns the TRACEABLE provenance graph for a table (#231): nodes (tables/datasets) and directed derivation edges, each edge carrying the producing record (run/transform/audit) that created the downstream table. Every node/edge outside the subject's read scope is dropped so the graph never discloses tables the caller cannot see. A table with no lineage returns its own node and empty:true.","parameters":[{"in":"path","name":"fqn","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"edges":[{"from":"cedms.l.silver","producedBy":{"actor":"flow:ingest","kind":"run","link":"/api/v1/usage/runs?run_ref=run-123","op":"sql","runRef":"run-123"},"to":"cedms.l.gold"}],"empty":false,"nodes":[{"fqn":"cedms.l.silver","id":"cedms.l.silver","isRoot":false,"kind":"table","name":"silver"},{"fqn":"cedms.l.gold","id":"cedms.l.gold","isRoot":true,"kind":"table","name":"gold"}],"root":"cedms.l.gold"},"schema":{"properties":{"edges":{"description":"Derivations: {from,to,producedBy}; producedBy={kind,op?,runRef?,actor?,at?,sql?,link} where link opens the existing run ledger or audit-log record","items":{"type":"object"},"type":"array"},"empty":{"description":"True when the table has no lineage edges (honest empty state)","type":"boolean"},"nodes":{"description":"Tables/datasets: {id,fqn,name,kind,isRoot}","items":{"type":"object"},"type":"array"},"root":{"description":"The root table FQN the graph is centered on","type":"string"}},"required":["root","nodes","edges","empty"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the TRACEABLE provenance graph for a table (#231): nodes (tables/datasets) and directed derivation edges, each edge carrying the producing record (run/transform/audit) that created the downstream table.","tags":["Catalog"]}},"/api/v1/classifications/taxonomy":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Classifications"],"x-stability":"experimental"},"put":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Classifications"],"x-stability":"experimental"}},"/api/v1/compute/sql":{"post":{"description":"Runs a SQL query against the tenant's lakehouse tables and returns the result rows. It ReBAC-gates the parsed FROM target (requires `read`), rejects namespaces mid-deletion (404) and tables the cached catalog DAG doesn't know (400, before provisioning billed compute), then executes on either the embedded single-table SQLite engine or, when distributed mode is enabled, on Spark via Livy with logical FQNs rewritten to per-tenant Iceberg identifiers; serverless compute is provisioned on demand and metered. A CREATE TABLE / CTAS statement also registers its target table in the catalog so produced tables become browsable.","requestBody":{"content":{"application/json":{"example":{"sql":"SELECT event_id, status, flow_id FROM orchestration.projected.run_started LIMIT 2"},"schema":{"properties":{"sql":{"description":"The SQL to run. Embedded engine supports single-table SELECTs (projection/WHERE/GROUP BY/aggregates/ORDER BY/LIMIT) over a 3-level FQN catalog.schema.table; distributed mode also runs CREATE TABLE/CTAS.","type":"string"},"warehouse_id":{"description":"Compute warehouse to run on; empty means serverless (on-demand, scale-to-zero, metered). Decoded but the selection is effectively informational on this path.","type":"string"}},"required":["sql"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"engine":"livy-spark","rows":[{"event_id":"2cee5a92-b9d3-4891-87fa-9b19da89b415","flow_id":"flow-a0015734","status":"running"},{"event_id":"677e9942-375c-44e9-b8ae-f11002cfd79b","flow_id":"flow-a0015734","status":"running"}]},"schema":{"properties":{"engine":{"description":"The engine that actually ran the request: \"embedded-sqlite\" or \"livy-spark\" when distributed mode is on.","type":"string"},"rows":{"description":"Result rows, each a flat JSON object of column name to string value (domain.Row marshals to its flat value map, not a nested Values object).","items":{"type":"object"},"type":"array"}},"required":["rows","engine"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs a SQL query against the tenant's lakehouse tables and returns the result rows.","tags":["Compute"]}},"/api/v1/compute/sql/cancel":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Compute"],"x-stability":"experimental"}},"/api/v1/compute/sql/distributed":{"post":{"description":"Runs a SQL query on the distributed Spark/Livy engine and returns its result rows — the distributed counterpart to POST /api/v1/compute/sql. It fails closed: it extracts every referenced catalog.schema.table, rejects a query that names no recognizable 3-level table (400), requires ReBAC `read` on all referenced tables, then tenantizes the logical FQNs to physical per-tenant Iceberg identifiers before executing with a 5-minute per-request timeout.","requestBody":{"content":{"application/json":{"example":{"sql":"SELECT event_id, status FROM orchestration.projected.run_started LIMIT 2"},"schema":{"properties":{"sql":{"description":"The SQL to run on Spark. Must reference at least one 3-level catalog.schema.table (Spark can read every referenced table, so all are authz-checked).","type":"string"}},"required":["sql"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"engine":"spark-livy","rows":[{"event_id":"2cee5a92-b9d3-4891-87fa-9b19da89b415","status":"running"},{"event_id":"677e9942-375c-44e9-b8ae-f11002cfd79b","status":"running"}]},"schema":{"properties":{"engine":{"description":"Always the literal \"spark-livy\" on this endpoint.","type":"string"},"rows":{"description":"Result rows, each a flat JSON object of column name to string value.","items":{"type":"object"},"type":"array"}},"required":["rows","engine"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs a SQL query on the distributed Spark/Livy engine and returns its result rows — the distributed counterpart to POST /api/v1/compute/sql.","tags":["Compute"]}},"/api/v1/compute/sql/repair":{"post":{"description":"Mnemo SQL repair tool: given a query that failed and the error it produced, it asks the governed chat model for a corrected query and returns ONLY the fixed SQL (the editor swaps it in). The system prompt constrains the model to standard SQLite-compatible single-table SELECTs over a 3-level FQN with no joins or cross-table subqueries.","requestBody":{"content":{"application/json":{"example":{"error":"syntax error near SELCT","sql":"SELCT * FRM orchestration.projected.run_started"},"schema":{"properties":{"error":{"description":"The error message the failed query produced, included in the prompt to guide the correction.","type":"string"},"sql":{"description":"The SQL query that failed; rejected with 400 if empty/whitespace.","type":"string"}},"required":["sql"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"sql":"SELECT * FROM orchestration.projected.run_started"},"schema":{"properties":{"sql":{"description":"The corrected SQL query, cleaned of markdown fences and trailing semicolons via cleanSQL.","type":"string"}},"required":["sql"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Mnemo SQL repair tool: given a query that failed and the error it produced, it asks the governed chat model for a corrected query and returns ONLY the fixed SQL (the editor swaps it in).","tags":["Compute"],"x-stability":"experimental"}},"/api/v1/connections":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Connections"],"x-stability":"experimental"}},"/api/v1/connections/access":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Connections"],"x-stability":"experimental"}},"/api/v1/connectors":{"get":{"description":"Lists every managed connector in the caller's tenant, alphabetically by name. It enumerates all objects under the tenant's `connectors/` prefix in the object store, unmarshals each into a connector record, silently skips any that fail to load or decode, and returns the sorted set.","responses":{"200":{"content":{"application/json":{"example":{"connectors":[{"name":"crm.contacts","schema":{"columns":[{"name":"email","type":"string"},{"name":"id","type":"string"}]},"source_type":"salesforce","updated_at":"2026-07-14T18:20:11Z","updated_by":"eng@edgentllc.com"}]},"schema":{"properties":{"connectors":{"description":"Array of connector records, sorted ascending by name; empty array when none exist.","items":{"type":"object"},"type":"array"},"connectors[].name":{"description":"Connector name (dot-joined identifier segments).","type":"string"},"connectors[].schema":{"description":"Declared normalized relational schema: {columns:[{name,type}]}.","type":"object"},"connectors[].source_type":{"description":"Source system type, e.g. postgres, salesforce, stripe.","type":"string"},"connectors[].updated_at":{"description":"RFC3339 UTC timestamp of last write.","type":"string"},"connectors[].updated_by":{"description":"Subject that last wrote the connector; omitted when empty.","type":"string"}},"required":["connectors","connectors[].name","connectors[].source_type","connectors[].schema","connectors[].updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists every managed connector in the caller's tenant, alphabetically by name.","tags":["Connectors"]}},"/api/v1/connectors/{name}":{"delete":{"description":"Deletes a managed connector by name from the caller's tenant object store. It is idempotent — deleting a name that does not exist still succeeds — and returns 204 No Content with an empty body on success.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"description":"Path param: connector name; dot-joined identifier segments, \u003c=200 chars, else 400.","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes a managed connector by name from the caller's tenant object store.","tags":["Connectors"]},"get":{"description":"Returns a single managed connector by name, including its source type and declared normalized schema. Loads the object at `connectors/{name}.json` from the tenant's object store and returns 404 when it is absent or fails to decode.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"description":"Path param: connector name; must match dot-joined identifier segments ([A-Za-z0-9_-]+ joined by dots), \u003c=200 chars, else 400.","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"name":"crm.contacts","schema":{"columns":[{"name":"email","type":"string"},{"name":"id","type":"string"}]},"source_type":"salesforce","updated_at":"2026-07-14T18:20:11Z","updated_by":"eng@edgentllc.com"},"schema":{"properties":{"name":{"description":"Connector name.","type":"string"},"schema":{"description":"Declared normalized relational schema: {columns:[{name,type}]}.","type":"object"},"source_type":{"description":"Source system type, e.g. postgres, salesforce, stripe.","type":"string"},"updated_at":{"description":"RFC3339 UTC timestamp of last write.","type":"string"},"updated_by":{"description":"Subject that last wrote the connector; omitted when empty.","type":"string"}},"required":["name","source_type","schema","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns a single managed connector by name, including its source type and declared normalized schema.","tags":["Connectors"]},"put":{"description":"Creates or replaces a managed connector and its declared normalized schema for the caller's tenant. It validates the name and that every schema column carries a non-empty name, stamps updated_by (the authenticated subject) and updated_at (now, RFC3339 UTC), marshals the full connector record and persists it to the object store, then echoes the stored record.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"schema":{"columns":[{"name":"id","type":"string"},{"name":"email","type":"string"}]},"source_type":"salesforce"},"schema":{"properties":{"name":{"description":"Path param: connector name; dot-joined identifier segments, \u003c=200 chars.","type":"string"},"schema":{"description":"Normalized schema {columns:[{name,type}]}; if present, every column must have a non-empty name or the request is rejected 400.","type":"object"},"schema.columns":{"description":"Column list; each element is {name:string, type:string}.","items":{"type":"object"},"type":"array"},"source_type":{"description":"Source system type (trimmed on save), e.g. postgres, salesforce, stripe. No enum enforced.","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"name":"crm.contacts","schema":{"columns":[{"name":"id","type":"string"},{"name":"email","type":"string"}]},"source_type":"salesforce","updated_at":"2026-07-15T00:00:00Z","updated_by":"eng@edgentllc.com"},"schema":{"properties":{"name":{"description":"Connector name from the path.","type":"string"},"schema":{"description":"The declared schema as stored.","type":"object"},"source_type":{"description":"Trimmed source type as stored.","type":"string"},"updated_at":{"description":"RFC3339 UTC write timestamp.","type":"string"},"updated_by":{"description":"Authenticated subject that made this write; omitted when empty (e.g. anonymous with authz disabled).","type":"string"}},"required":["name","source_type","schema","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates or replaces a managed connector and its declared normalized schema for the caller's tenant.","tags":["Connectors"]}},"/api/v1/connectors/{name}/reconcile":{"post":{"description":"Diffs an observed source schema against the connector's declared schema and returns the drift and reconciliation plan: additive columns widen the schema (non-breaking, auto-appliable), while removed columns and type changes are recorded as breaking reasons that block auto-apply. With the query param apply=true, when the plan is non-breaking and the drift is non-empty, it also widens and persists the connector's stored schema and reports applied=true.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"observed":{"columns":[{"name":"id","type":"string"},{"name":"email","type":"string"},{"name":"created_at","type":"timestamp"}]}},"schema":{"properties":{"apply":{"description":"Query param; when literal string 'true' and the plan is auto-appliable with non-empty drift, the widened schema is persisted.","type":"boolean"},"name":{"description":"Path param: connector name; must reference an existing connector (else 404).","type":"string"},"observed":{"description":"The source's currently-observed schema {columns:[{name,type}]}, diffed against the connector's declared schema. Decode failure (incl. empty body) -\u003e 400; a present-but-empty object diffs against empty (declared columns all read as removed).","type":"object"}},"required":["name","observed"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"applied":false,"plan":{"auto_apply":true,"breaking":[],"drift":{"added":[{"name":"created_at","type":"timestamp"}],"removed":null,"type_changed":null},"new_schema":{"columns":[{"name":"created_at","type":"timestamp"},{"name":"email","type":"string"},{"name":"id","type":"string"}]}}},"schema":{"properties":{"applied":{"description":"Whether the stored connector schema was actually widened and persisted this call (requires apply=true, auto_apply=true, and non-empty drift).","type":"boolean"},"plan":{"description":"The reconciliation plan (schemadrift.Plan).","type":"object"},"plan.auto_apply":{"description":"True iff there are no breaking changes.","type":"boolean"},"plan.breaking":{"description":"Human-readable breaking-change reasons (removed columns and type changes); [] when none (explicitly initialized, so it is an empty array, not null).","items":{"type":"string"},"type":"array"},"plan.drift":{"description":"Structural diff observed-vs-declared: {added:[{name,type}], removed:[{name,type}], type_changed:[{name,from,to}]}, each sorted by name. Each of the three arrays serializes as JSON null (not []) when it has no entries (nil slice, no omitempty).","type":"object"},"plan.new_schema":{"description":"Declared schema widened with additive columns (breaking-removed columns are kept so downstream isn't silently broken); {columns:[{name,type}]} sorted by name.","type":"object"}},"required":["plan","plan.drift","plan.new_schema","plan.breaking","plan.auto_apply","applied"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Diffs an observed source schema against the connector's declared schema and returns the drift and reconciliation plan: additive columns widen the schema (non-breaking, auto-appliable), while removed columns and type changes are recorded as breaking reasons that block auto-apply.","tags":["Connectors"]}},"/api/v1/connectors/{name}/run":{"post":{"parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Connectors"],"x-stability":"experimental"}},"/api/v1/connectors/{name}/runs":{"get":{"parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Connectors"],"x-stability":"experimental"}},"/api/v1/dashboards":{"get":{"description":"Lists all dashboards saved under the calling tenant's dashboards/ object-store prefix, returning a lightweight summary of each (id, name, widget count, last-updated timestamp) rather than the full widget bodies. Results are sorted by UpdatedAt descending so the most recently edited dashboards appear first.","responses":{"200":{"content":{"application/json":{"example":{"dashboards":[{"id":"3db5763a2c1ad128","name":"VForce360 Platform Operations","updated_at":"2026-07-04T16:15:56.660753235Z","widget_count":8},{"id":"c61ec9edc405d5b1","name":"Population \u0026 Economy Explorer","updated_at":"2026-07-04T16:15:23.168457018Z","widget_count":4},{"id":"2d2c6e7dd6ffcd66","name":"Orchestration Health","updated_at":"2026-07-04T16:15:18.74338456Z","widget_count":4},{"id":"f4f36a5f48bf4f9c","name":"Weather \u0026 Field Ops","updated_at":"2026-07-04T16:15:11.315117559Z","widget_count":4},{"id":"b68394b6db9d696e","name":"Global Development Overview","updated_at":"2026-07-04T16:14:58.247174916Z","widget_count":4}]},"schema":{"properties":{"dashboards":{"description":"Array of dashboard summaries, newest-updated first.","items":{"type":"object"},"type":"array"},"dashboards[].id":{"description":"Dashboard identifier (16-hex-char id).","type":"string"},"dashboards[].name":{"description":"Human-readable dashboard name.","type":"string"},"dashboards[].updated_at":{"description":"RFC3339Nano timestamp of last update; also the sort key.","type":"string"},"dashboards[].widget_count":{"description":"Number of widgets on the dashboard.","type":"integer"}},"required":["dashboards","dashboards[].id","dashboards[].name","dashboards[].widget_count","dashboards[].updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists all dashboards saved under the calling tenant's dashboards/ object-store prefix, returning a lightweight summary of each (id, name, widget count, last-updated timestamp) rather than the full widget bodies.","tags":["Dashboards"]},"post":{"description":"Creates a new, empty dashboard: it decodes only a name, mints a fresh id, sets created_by to the request subject and created_at/updated_at to now, initializes an empty widgets array, and persists the record under dashboards/{id}.json. Widgets are not created here; add them later via PUT.","requestBody":{"content":{"application/json":{"example":{"name":"Orchestration Health"},"schema":{"properties":{"name":{"description":"Dashboard name; trimmed, and defaults to \"Untitled dashboard\" when empty or omitted.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-15T12:00:00Z","created_by":"sofia.admin","id":"a1b2c3d4e5f60718","name":"Orchestration Health","updated_at":"2026-07-15T12:00:00Z","widgets":[]},"schema":{"properties":{"created_at":{"description":"RFC3339Nano creation timestamp.","type":"string"},"created_by":{"description":"Subject that created the dashboard (from request auth context).","type":"string"},"id":{"description":"Newly minted dashboard id (16-hex-char).","type":"string"},"name":{"description":"Dashboard name (or \"Untitled dashboard\").","type":"string"},"updated_at":{"description":"RFC3339Nano timestamp, equal to created_at on create.","type":"string"},"widgets":{"description":"Always an empty array on create (initialized to []widget{}).","items":{"type":"object"},"type":"array"}},"required":["id","name","widgets","created_by","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates a new, empty dashboard: it decodes only a name, mints a fresh id, sets created_by to the request subject and created_at/updated_at to now, initializes an empty widgets array, and persists the record under dashboards/{id}.json.","tags":["Dashboards"]}},"/api/v1/dashboards/{id}":{"delete":{"description":"Deletes the dashboard record at dashboards/{id}.json from the tenant's object store. Returns no body on success.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the dashboard id to delete.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes the dashboard record at dashboards/{id}.json from the tenant's object store.","tags":["Dashboards"]},"get":{"description":"Fetches one dashboard by id from the tenant's dashboards/ prefix and returns its full body including every widget (SQL, visualization type, axis keys). The widget SQL is not executed here; the client renders each widget by separately calling the /compute/sql endpoint.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the dashboard id to fetch.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-04T16:15:18.710897592Z","created_by":"sofia.admin","id":"2d2c6e7dd6ffcd66","name":"Orchestration Health","updated_at":"2026-07-04T16:15:18.74338456Z","widgets":[{"id":"w-667c2150","sql":"SELECT status, count(*) AS n FROM orchestration.projected.run_completed GROUP BY status ORDER BY n DESC","title":"Runs by Status","viz":"bar","x_key":"status","y_key":"n"},{"id":"w-4bfe2901","sql":"SELECT avg(duration_ms) AS avg_ms FROM orchestration.projected.run_completed","title":"Avg Duration (ms)","viz":"counter"},{"id":"w-852b04ac","sql":"SELECT run_id, flow_id, status, duration_ms FROM orchestration.projected.run_completed ORDER BY time DESC LIMIT 15","title":"Recent Completed Runs","viz":"table"},{"id":"w-f9478dff","sql":"SELECT count(*) AS n FROM isd.projected.course_authored","title":"Courses Authored","viz":"counter"}]},"schema":{"properties":{"created_at":{"description":"RFC3339Nano creation timestamp.","type":"string"},"created_by":{"description":"Subject that created the dashboard.","type":"string"},"id":{"description":"Dashboard identifier.","type":"string"},"name":{"description":"Dashboard name.","type":"string"},"updated_at":{"description":"RFC3339Nano last-update timestamp.","type":"string"},"widgets":{"description":"The dashboard's widgets.","items":{"type":"object"},"type":"array"},"widgets[].id":{"description":"Widget identifier.","type":"string"},"widgets[].sql":{"description":"The SQL query that backs the widget (run client-side via /compute/sql).","type":"string"},"widgets[].title":{"description":"Widget title shown above the visualization.","type":"string"},"widgets[].viz":{"description":"Visualization type: table | bar | line | counter.","type":"string"},"widgets[].x_key":{"description":"Result column used for the X axis / category (omitted via omitempty for counter/table).","type":"string"},"widgets[].y_key":{"description":"Result column used for the Y axis / value (omitted via omitempty for counter/table).","type":"string"}},"required":["id","name","widgets","widgets[].id","widgets[].title","widgets[].sql","widgets[].viz","created_by","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Fetches one dashboard by id from the tenant's dashboards/ prefix and returns its full body including every widget (SQL, visualization type, axis keys).","tags":["Dashboards"]},"put":{"description":"Replaces the name and full widget list of the dashboard at {id}, then re-persists it with a bumped updated_at. This is a full overwrite of widgets (not a merge); it preserves the original created_by/created_at by first loading the existing record, and if none exists it effectively creates the dashboard at that id with created_at/created_by set to now/the caller.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"name":"Orchestration Health","widgets":[{"id":"w-667c2150","sql":"SELECT status, count(*) AS n FROM orchestration.projected.run_completed GROUP BY status ORDER BY n DESC","title":"Runs by Status","viz":"bar","x_key":"status","y_key":"n"},{"id":"w-4bfe2901","sql":"SELECT avg(duration_ms) AS avg_ms FROM orchestration.projected.run_completed","title":"Avg Duration (ms)","viz":"counter"}]},"schema":{"properties":{"id":{"description":"Path parameter: the dashboard id to update (becomes the stored ID).","type":"string"},"name":{"description":"New dashboard name; trimmed, defaults to \"Untitled dashboard\" when empty.","type":"string"},"widgets":{"description":"Full replacement widget list; each widget is {id,title,sql,viz,x_key?,y_key?}. Omitting it stores JSON null (not []).","items":{"type":"object"},"type":"array"},"widgets[].id":{"description":"Widget identifier.","type":"string"},"widgets[].sql":{"description":"SQL backing the widget.","type":"string"},"widgets[].title":{"description":"Widget title.","type":"string"},"widgets[].viz":{"description":"Visualization type: table | bar | line | counter.","type":"string"},"widgets[].x_key":{"description":"X-axis / category result column (omitempty).","type":"string"},"widgets[].y_key":{"description":"Y-axis / value result column (omitempty).","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-04T16:15:18.710897592Z","created_by":"sofia.admin","id":"2d2c6e7dd6ffcd66","name":"Orchestration Health","updated_at":"2026-07-15T12:05:00Z","widgets":[{"id":"w-667c2150","sql":"SELECT status, count(*) AS n FROM orchestration.projected.run_completed GROUP BY status ORDER BY n DESC","title":"Runs by Status","viz":"bar","x_key":"status","y_key":"n"},{"id":"w-4bfe2901","sql":"SELECT avg(duration_ms) AS avg_ms FROM orchestration.projected.run_completed","title":"Avg Duration (ms)","viz":"counter"}]},"schema":{"properties":{"created_at":{"description":"Preserved creation timestamp, or now if newly created.","type":"string"},"created_by":{"description":"Preserved from the existing record, or the caller's subject if newly created.","type":"string"},"id":{"description":"Dashboard id (taken from the path).","type":"string"},"name":{"description":"Updated name (or \"Untitled dashboard\").","type":"string"},"updated_at":{"description":"RFC3339Nano timestamp set to now on every update.","type":"string"},"widgets":{"description":"The full widget list as saved (echoed from the request body); JSON null if the request omitted/nulled widgets.","items":{"type":"object"},"type":"array"}},"required":["id","name","widgets","created_by","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Replaces the name and full widget list of the dashboard at {id}, then re-persists it with a bumped updated_at.","tags":["Dashboards"]}},"/api/v1/demo/roles":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Demo"],"x-stability":"experimental"}},"/api/v1/demo/roles/switch":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Demo"],"x-stability":"experimental"}},"/api/v1/docs/endpoints":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"This manifest: the generated API surface.","tags":["Platform"]}},"/api/v1/docs/openapi.json":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"The same surface as an OpenAPI 3.0 document.","tags":["Platform"]}},"/api/v1/document":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"One processed document's full gold row including OCR text, by ?key= (keys contain slashes, so the key rides a query param like /object-image).","tags":["Documents"]}},"/api/v1/document-batches":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Document batches"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Document batches"],"x-stability":"experimental"}},"/api/v1/document-batches/purge":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Document batches"],"x-stability":"experimental"}},"/api/v1/document-batches/{id}":{"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Document batches"],"x-stability":"experimental"},"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Document batches"],"x-stability":"experimental"}},"/api/v1/document-batches/{id}/docs":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Document batches"],"x-stability":"experimental"}},"/api/v1/document-batches/{id}/timing":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Batch timing from RECORDED job timestamps (#1948): batch {first_enqueued_at,started_at,finished_at,work_ms,done,failed} — wall-clock and summed active work as SEPARATE quantities — plus completions[{key,finished_at,work_ms}] keyset-paged chronologically (?cursor=\u0026limit=, default 1000, cap 5000).","tags":["Documents"]}},"/api/v1/document-batches/{id}/undelete":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Document batches"],"x-stability":"experimental"}},"/api/v1/documents":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Paged + faceted documents list (no OCR text). Params: offset, limit (default 50, cap 200), doc_type (comma-set; 'unlabeled' = empty, and the pre-#1371 spelling 'unclassified' still matches it), disposition ('excluded' shows the shelf, hidden otherwise), batch, q (substring over key+doc_type+summary), sort (key|doc_type|confidence|date|chars), dir (asc|desc). Returns documents[], total, offset, limit, facets.doc_types[].","tags":["Documents"]}},"/api/v1/documents/categories":{"get":{"description":"Returns the processed-page counts grouped by doc_type — the data behind the Documents nav tree and category filters. It dogfoods the embedded SQL engine by running a GROUP BY over the gold table (cedms.microfiche.pages), excluding operator soft-deletes (disposition='excluded'), and returns each doc_type with its count plus a grand total. A missing/empty gold table (or an unconfigured compute engine) yields an empty list with a 200, never an error, so the nav renders cleanly before anything is ingested.","responses":{"200":{"content":{"application/json":{"example":{"categories":[],"total":0},"schema":{"properties":{"categories":{"description":"Array of category buckets, ordered by count descending then doc_type ascending; empty when nothing has been processed.","items":{"type":"object"},"type":"array"},"categories[].count":{"description":"Number of non-excluded pages of this doc_type.","type":"integer"},"categories[].doc_type":{"description":"The document type/category; rows with an empty doc_type are reported as \"unclassified\".","type":"string"},"total":{"description":"Sum of all category counts (total non-excluded pages).","type":"integer"}},"required":["categories","categories[].doc_type","categories[].count","total"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the processed-page counts grouped by doc_type — the data behind the Documents nav tree and category filters.","tags":["Documents"]}},"/api/v1/documents/keywords":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Documents"],"x-stability":"experimental"}},"/api/v1/documents/reprocess":{"post":{"description":"Bulk reprocess for failed/stalled documents (#1615): flips the selected ingest jobs back to 'queued' with error cleared and ATTEMPTS RESET to 0, so a stalled page gets its full retry budget back and the worker tier re-runs each page as its own job (smallest unit of work). Selection is EITHER an explicit keys list (cap 500) OR the server-side filter form {batch, status} — 'reprocess all stalled in this batch' — so nobody pages through thousands of rows to select them. Running and already-queued jobs are left alone (never yank a live page; idempotent). Tenant-scoped like the single-page reprocess.","requestBody":{"content":{"application/json":{"example":{"batch":"b1","status":"failed,stalled"},"schema":{"properties":{"batch":{"description":"Filter form: the batch whose jobs to requeue. Required together with status.","type":"string"},"keys":{"description":"Explicit page keys to requeue. Capped at 500 (400 above the cap — use the filter form). Mutually exclusive with batch/status.","items":{"type":"string"},"type":"array"},"model":{"description":"Optional governed OCR-model override applied to every requeued job; validated against the catalog BEFORE any state changes (403 with the reason, same residency semantics as the single-page reprocess). Empty keeps each job's original model.","type":"string"},"status":{"description":"Filter form: comma-separated facet statuses to requeue (processing, queued, running, done, failed, stalled, stuck; 'processing' expands to queued+running). Unknown values are a 400.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"requeued":12},"schema":{"properties":{"requeued":{"description":"How many jobs were flipped back to queued. 0 is not an error (nothing matched, or everything was running/queued already).","type":"integer"}},"required":["requeued"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Bulk reprocess for failed/stalled documents (#1615): flips the selected ingest jobs back to 'queued' with error cleared and ATTEMPTS RESET to 0, so a stalled page gets its full retry budget back and the worker tier re-runs each page as its own job (smallest unit of work).","tags":["Documents"]}},"/api/v1/documents/status-facets":{"get":{"description":"Returns the per-status counts of the tenant's ingest jobs (queued, running, done, failed, stalled, stuck) with an attempts (retry-count) breakdown per status — the documents surface's status facets (#1615). Stalled is split from failed by the queue reaper's error prefix ('stalled: exceeded max processing attempts'). Computed from the warm side (ingest_jobs) in ONE grouped aggregate — never a corpus or object-store scan. Archive control jobs are excluded, mirroring every other batch count.","requestBody":{"content":{"application/json":{"schema":{"properties":{"batch":{"description":"Query param: scope the facets to one batch's jobs; empty = the whole tenant.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"batch":"b1","facets":[{"count":180,"status":"done"},{"attempts":[{"attempts":1,"count":1},{"attempts":2,"count":3}],"count":4,"status":"failed"},{"attempts":[{"attempts":3,"count":12}],"count":12,"status":"stalled"}]},"schema":{"properties":{"batch":{"description":"The batch scope echoed back (empty for tenant-wide).","type":"string"},"facets":{"description":"Per-status roll-ups: {status, count, attempts:[{attempts, count}]}, attempts buckets ascending. Empty when the queue has no bulk-reprocess capability (in-memory test fakes).","items":{"type":"string"},"type":"array"}},"required":["facets","batch"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the per-status counts of the tenant's ingest jobs (queued, running, done, failed, stalled, stuck) with an attempts (retry-count) breakdown per status — the documents surface's status facets (#1615).","tags":["Documents"]}},"/api/v1/documents/stream":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Tenant-scoped SSE stream: page.processed {key,doc_type,disposition,confidence} in real time (sync process path) and batch.updated {batch_id,stats} via a 3s per-connection diff poll; with ?batch=\u003cid\u003e, doc.processed additionally carries started_at/finished_at (unix ms) — the queue's RECORDED work window, never the frame's emit time (ts is stale-frame gating only). Heartbeat comment every 25s.","tags":["Documents"]}},"/api/v1/documents/tags":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Documents"],"x-stability":"experimental"}},"/api/v1/documents/{key}/cancel":{"post":{"parameters":[{"in":"path","name":"key","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Documents"],"x-stability":"experimental"}},"/api/v1/documents/{key}/lifecycle":{"get":{"parameters":[{"in":"path","name":"key","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Documents"],"x-stability":"experimental"}},"/api/v1/documents/{key}/requeue":{"post":{"parameters":[{"in":"path","name":"key","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Documents"],"x-stability":"experimental"}},"/api/v1/domains/assets":{"get":{"description":"Enumerates every catalog table assigned to a given data domain (via the ?domain= query parameter), returning each table's stored ownership record (typed owners, domain, and audit metadata). It lists all ownership records for the tenant, keeps only those whose Domain matches the requested name case-insensitively, then filters the set down to just the tables the calling subject is authorized to read before sorting the results by fully-qualified name.","requestBody":{"content":{"application/json":{"schema":{"properties":{"domain":{"description":"Query param. The data domain/community name to enumerate assets for; matched case-insensitively against each record's domain. Required at runtime — a blank or missing value returns HTTP 400, but it is a query param so listed as not-required per the no-body-GET convention.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"assets":[],"domain":"marketing"},"schema":{"properties":{"assets":{"description":"The readable ownership records in this domain, sorted ascending by fqn. Each element is a tableOwnership object; empty array when the domain has no readable assets.","items":{"type":"object"},"type":"array"},"assets[].domain":{"description":"The domain this asset belongs to (omitempty). Matches the requested domain.","type":"string"},"assets[].fqn":{"description":"Fully-qualified table identifier (catalog.schema.table) the ownership record applies to.","type":"string"},"assets[].owners":{"description":"Typed owner assignments; each has principal (string) and role (one of technical, business, steward, expert). No omitempty on this field, so the key is always present (may be null/empty).","items":{"type":"object"},"type":"array"},"assets[].updated_at":{"description":"RFC3339 UTC timestamp of the last ownership write (omitempty).","type":"string"},"assets[].updated_by":{"description":"Subject who last wrote the ownership record (omitempty).","type":"string"},"domain":{"description":"Echoes the requested domain name (the raw ?domain= value, trimmed).","type":"string"}},"required":["domain","assets","assets[].fqn"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Enumerates every catalog table assigned to a given data domain (via the ?domain= query parameter), returning each table's stored ownership record (typed owners, domain, and audit metadata).","tags":["Domains"]}},"/api/v1/education/schema-packs":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Education"],"x-stability":"experimental"}},"/api/v1/education/schema-packs/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Education"],"x-stability":"experimental"}},"/api/v1/education/schema-packs/{id}/apply":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Education"],"x-stability":"experimental"}},"/api/v1/estate/reindex":{"post":{"description":"Rebuilds the calling tenant's 'estate memory' on demand by walking the same ReBAC-filtered readable snapshot Mnemo grounds on, running a read-only SELECT over each table to compute its row count and categorical breakdown, and upserting those facts into the per-tenant vector index that the 'ask your data' engine recalls from. Projection is best-effort per table (any table that cannot be scanned or whose upsert fails is silently skipped, never wedging the caller), and the same facts are also lazily backfilled on the first Mnemo ask for a tenant that has zero facts.","responses":{"200":{"content":{"application/json":{"example":{"facts":12,"tables":3},"schema":{"properties":{"facts":{"description":"Total number of estate facts upserted across all projected tables.","type":"integer"},"tables":{"description":"Number of tables successfully projected into estate facts (skipped/unscannable tables are not counted).","type":"integer"}},"required":["tables","facts"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Rebuilds the calling tenant's 'estate memory' on demand by walking the same ReBAC-filtered readable snapshot Mnemo grounds on, running a read-only SELECT over each table to compute its row count and categorical breakdown, and upserting those facts into the per-tenant vector index that the 'ask your data' engine recalls from.","tags":["Estate"]}},"/api/v1/events/ingest":{"post":{"description":"Inbound domain-event sink: accepts a platform CloudEvents 1.0 envelope (or a JSON array of them as a batch) and archives each as a gold row in the lakehouse, routed to a destination table by the event's `type` (via eventsink.RouteFor) rather than by the delivery topic. Fed by a DAPR pub/sub subscription, a broker relay, or a direct POST; the owning tenant is resolved from the event payload's own `tenantId`/`tenant_id` (falling back to the caller's tenant) so a system/relay delivery still lands in the right tenant's catalog, and the destination table is auto-registered in the catalog and estate-memory projection is refreshed best-effort. Returns 202 Accepted since the producer already committed, but a genuine flatten/ingest failure returns 4xx so DAPR can redeliver.","requestBody":{"content":{"application/json":{"example":{"data":{"flow":"pingmapper","runId":"r-42","tenantId":"acme"},"datacontenttype":"application/json","id":"c1f2e3d4-0001","source":"/vforce-flow","specversion":"1.0","subject":"run/r-42","time":"2026-07-15T00:00:00Z","traceparent":"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01","type":"ai.vforce360.flow.run.started"},"schema":{"properties":{"data":{"description":"Event payload; if not a JSON object it is treated as empty. `tenantId`/`tenant_id` inside it resolve the owning tenant; the whole object round-trips into the row's data column.","type":"object"},"datacontenttype":{"description":"CloudEvents data content type; archived verbatim.","type":"string"},"id":{"description":"CloudEvents event id; archived verbatim, not validated.","type":"string"},"source":{"description":"CloudEvents source URI; archived verbatim.","type":"string"},"specversion":{"description":"CloudEvents spec version; defaults to \"1.0\" when omitted.","type":"string"},"subject":{"description":"CloudEvents subject; archived verbatim.","type":"string"},"time":{"description":"CloudEvents timestamp; archived verbatim.","type":"string"},"traceparent":{"description":"W3C traceparent (CloudEvents Distributed Tracing extension) read from the envelope top level; archived if present.","type":"string"},"tracestate":{"description":"W3C tracestate (CloudEvents Distributed Tracing extension) read from the envelope top level; archived if present.","type":"string"},"type":{"description":"CloudEvents type; REQUIRED (empty =\u003e 400) and is the routing key that selects the destination table.","type":"string"}},"required":["type"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"rows":1,"table":"orchestration.events.flow_runs","type":"ai.vforce360.flow.run.started"},"schema":{"properties":{"batch":{"description":"BATCH RESPONSE ONLY: number of elements in the submitted array.","type":"integer"},"events":{"description":"BATCH RESPONSE ONLY: per-element single-event result objects (each {table,rows,type}).","items":{"type":"object"},"type":"array"},"rows":{"description":"Number of rows ingested (1 for a single event; total across all elements in a batch response).","type":"integer"},"table":{"description":"Fully-qualified destination table (catalog.schema.name) the event was routed to and landed in (single-event response).","type":"string"},"type":{"description":"Echo of the CloudEvent type that was routed (single-event response).","type":"string"}},"required":["table","rows","type"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Inbound domain-event sink: accepts a platform CloudEvents 1.0 envelope (or a JSON array of them as a batch) and archives each as a gold row in the lakehouse, routed to a destination table by the event's `type` (via eventsink.RouteFor) rather than by the delivery topic.","tags":["Events"]}},"/api/v1/extraction/chunk":{"post":{"description":"Layout-aware IDP element typing plus RAG chunking (#249): classifies each posted text block (in reading order) into a typed element (Title/NarrativeText/ListItem/Header/Footer) using text heuristics and cross-page boilerplate detection, then groups the elements into retrieval-tuned chunks by title with an optional size bound and character overlap. Stateless pure compute; Header/Footer elements are dropped from chunks so boilerplate does not pollute the vector index.","requestBody":{"content":{"application/json":{"example":{"blocks":[{"page":1,"text":"INTRODUCTION"},{"page":1,"text":"This is body text."}],"max_chars":0,"overlap":0},"schema":{"properties":{"blocks":{"description":"Document text blocks in reading order; each is {text:string, page:int(1-based)}. Must be non-empty and \u003c= 200000 blocks.","items":{"type":"object"},"type":"array"},"max_chars":{"description":"Max characters per chunk; \u003c=0 (default/omitted) disables splitting (one chunk per title section).","type":"integer"},"overlap":{"description":"Character overlap between split chunks; must be \u003e=0 and (when max_chars\u003e0) \u003c max_chars.","type":"integer"}},"required":["blocks"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"chunks":[{"page":1,"text":"This is body text.","title":"INTRODUCTION"}],"elements":[{"page":1,"text":"INTRODUCTION","type":"Title"},{"page":1,"text":"This is body text.","type":"NarrativeText"}]},"schema":{"properties":{"chunks":{"description":"Retrieval chunks: {title?:string, text:string, page:int(first element's page)}. Header/Footer elements are excluded.","items":{"type":"object"},"type":"array"},"elements":{"description":"Per-block classified elements: {type:string, text:string, page:int}. Same length/order as the input blocks.","items":{"type":"object"},"type":"array"}},"required":["elements","chunks"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Layout-aware IDP element typing plus RAG chunking (#249): classifies each posted text block (in reading order) into a typed element (Title/NarrativeText/ListItem/Header/Footer) using text heuristics and cross-page boilerplate detection, then groups the elements into retrieval-tuned chunks by title with an optional size bound and character overlap.","tags":["Extraction"]}},"/api/v1/extraction/evaluate":{"post":{"description":"Extraction-quality evaluation (#248): scores a labeled test set of field predictions against gold labels at a given confidence threshold and returns overall plus per-field precision/recall/F1 with exact-match semantics (a prediction counts only if it fires AND equals gold; a wrong fired prediction is both an FP and an FN). When optimize is true it also sweeps observed confidences to return the F1-maximizing threshold and its metrics.","requestBody":{"content":{"application/json":{"example":{"optimize":true,"predictions":[{"confidence":0.9,"field":"total","gold":"100","predicted":"100"},{"confidence":0.4,"field":"date","gold":"2021-01-01","predicted":"2021-01-01"}],"threshold":0.5},"schema":{"properties":{"optimize":{"description":"When true, also compute the F1-maximizing threshold (ties broken toward the lower threshold).","type":"boolean"},"predictions":{"description":"Labeled test set; each is {field:string, predicted:string(\"\"=nothing extracted), confidence:float[0,1], gold:string(\"\"=field not expected)}. Must be non-empty and \u003c= 100000.","items":{"type":"object"},"type":"array"},"threshold":{"description":"Confidence threshold at which predictions 'fire'; must be in [0,1] (default 0).","type":"number"}},"required":["predictions"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"best_metrics":{"f1":1,"fn":0,"fp":0,"precision":1,"recall":1,"threshold":0,"tp":2},"best_threshold":0,"overall":{"f1":0.6666666666666666,"fn":1,"fp":0,"precision":1,"recall":0.5,"threshold":0.5,"tp":1},"per_field":{"date":{"f1":0,"fn":1,"fp":0,"precision":0,"recall":0,"threshold":0.5,"tp":0},"total":{"f1":1,"fn":0,"fp":0,"precision":1,"recall":1,"threshold":0.5,"tp":1}}},"schema":{"properties":{"best_metrics":{"description":"Only present when optimize=true: Metrics at best_threshold.","type":"object"},"best_threshold":{"description":"Only present when optimize=true: the F1-maximizing threshold.","type":"number"},"overall":{"description":"Aggregate Metrics: {threshold,tp,fp,fn,precision,recall,f1}.","type":"object"},"per_field":{"description":"Metrics per field name, same shape as overall.","type":"string"}},"required":["overall","per_field"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Extraction-quality evaluation (#248): scores a labeled test set of field predictions against gold labels at a given confidence threshold and returns overall plus per-field precision/recall/F1 with exact-match semantics (a prediction counts only if it fires AND equals gold; a wrong fired prediction is both an FP and an FN).","tags":["Extraction"]}},"/api/v1/extraction/normalize":{"post":{"description":"Entity normalization (#243): turns a batch of raw extracted strings into canonical typed values — dates to ISO-8601 (YYYY-MM-DD), money to {amount,currency}, addresses to a single comma-separated canonical line. Each value is normalized independently; an unknown kind or an unparseable value is reported per-value (ok=false) so one bad entry does not fail the whole batch.","requestBody":{"content":{"application/json":{"example":{"day_first":false,"values":[{"field":"invoice_date","kind":"date","value":"03/04/2021"},{"field":"total","kind":"money","value":"$1,234.50"},{"field":"vendor_addr","kind":"address","value":"12 Main St\nSpringfield, IL"},{"field":"bad","kind":"phone","value":"x"}]},"schema":{"properties":{"day_first":{"description":"For ambiguous purely-numeric dates, interpret as DD/MM (European) instead of the default MM/DD (US). Applies to date kinds only.","type":"boolean"},"values":{"description":"Values to normalize; each is {field:string, kind:string, value:string}. kind is one of date/datetime/dob/expiry, money/currency/amount, or address. Must be non-empty and \u003c= 100000.","items":{"type":"object"},"type":"array"}},"required":["values"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"results":[{"field":"invoice_date","kind":"date","normalized":"2021-03-04","ok":true,"raw":"03/04/2021"},{"field":"total","kind":"money","normalized":{"amount":1234.5,"currency":"USD"},"ok":true,"raw":"$1,234.50"},{"field":"vendor_addr","kind":"address","normalized":"12 Main St, Springfield, IL","ok":true,"raw":"12 Main St\nSpringfield, IL"},{"error":"unknown normalization kind \"phone\"","field":"bad","kind":"phone","ok":false,"raw":"x"}]},"schema":{"properties":{"results":{"description":"One result per input value: {field, kind, raw, normalized?(string for date/address, {amount,currency} for money), ok:bool, error?(e.g. unknown kind)}.","items":{"type":"object"},"type":"array"}},"required":["results"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Entity normalization (#243): turns a batch of raw extracted strings into canonical typed values — dates to ISO-8601 (YYYY-MM-DD), money to {amount,currency}, addresses to a single comma-separated canonical line.","tags":["Extraction"]}},"/api/v1/extraction/route":{"post":{"description":"Per-field confidence routing (#242): given each extracted field's confidence and a per-field threshold policy, decides whether a document can be straight-through-processed ('auto') or must go to human 'review'. It is auto only when every required field is present AND every present field meets its threshold; otherwise review, naming the offending fields.","requestBody":{"content":{"application/json":{"example":{"fields":[{"confidence":0.98,"field":"total"},{"confidence":0.55,"field":"date"}],"policy":{"default_threshold":0.8,"required_fields":["vendor"],"thresholds":{"total":0.8}}},"schema":{"properties":{"fields":{"description":"Per-field confidences; each is {field:string, confidence:float[0,1]}. Must be non-empty.","items":{"type":"object"},"type":"array"},"policy":{"description":"Routing policy: {thresholds:map[string]float (per-field min confidence), default_threshold:float (for fields without an explicit threshold), required_fields:[]string (must be present and clear threshold)}.","type":"object"}},"required":["fields"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"decision":"review","low_confidence_fields":["date"],"missing_fields":["vendor"]},"schema":{"properties":{"decision":{"description":"'auto' or 'review'.","type":"string"},"low_confidence_fields":{"description":"Present fields below their threshold, sorted.","items":{"type":"string"},"type":"array"},"missing_fields":{"description":"Required fields that were absent, sorted.","items":{"type":"string"},"type":"array"}},"required":["decision","low_confidence_fields","missing_fields"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Per-field confidence routing (#242): given each extracted field's confidence and a per-field threshold policy, decides whether a document can be straight-through-processed ('auto') or must go to human 'review'.","tags":["Extraction"]}},"/api/v1/extraction/split":{"post":{"description":"Multi-document package splitting (#247): splits a single scanned page stream that actually contains several logical documents into those documents. Boundaries are drawn at separator pages (barcode/blank divider sheets, which are dropped) and at a class change from the previous content page; each output document's class is the majority class of its pages (ties broken by first occurrence).","requestBody":{"content":{"application/json":{"example":{"pages":[{"class":"invoice","index":0},{"class":"invoice","index":1},{"class":"","index":2,"separator":true},{"class":"receipt","index":3}]},"schema":{"properties":{"pages":{"description":"Pages in order; each is {index:int, class:string, separator?:bool}. Separator pages delimit documents and are excluded from output. Must be non-empty and \u003c= 100000.","items":{"type":"object"},"type":"array"}},"required":["pages"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"document_count":2,"documents":[{"class":"invoice","end_page":1,"pages":[0,1],"start_page":0},{"class":"receipt","end_page":3,"pages":[3],"start_page":3}]},"schema":{"properties":{"document_count":{"description":"Number of documents (len(documents)).","type":"integer"},"documents":{"description":"Contiguous logical documents: {class:string(majority), pages:[]int(indices in order), start_page:int, end_page:int}.","items":{"type":"object"},"type":"array"}},"required":["documents","document_count"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Multi-document package splitting (#247): splits a single scanned page stream that actually contains several logical documents into those documents.","tags":["Extraction"]}},"/api/v1/faces":{"get":{"description":"Returns one page of the tenant's enrolled face gallery (id + viewer image URL) for the 'pick an existing face' grid. Paged via limit/offset (default 48, cap 200) and carries the full `total` count for the pager, since the gallery can hold tens of thousands of faces after a bulk enrollment.","requestBody":{"content":{"application/json":{"schema":{"properties":{"limit":{"description":"Page size query param; default 48, hard-capped at 200.","type":"integer"},"offset":{"description":"Zero-based page offset query param; default 0.","type":"integer"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"faces":[{"id":"face-00-est80-senior","image_url":"/api/v1/faces/face-00-est80-senior/image","score":0},{"id":"face-01-est30-adult","image_url":"/api/v1/faces/face-01-est30-adult/image","score":0}],"limit":2,"offset":0,"total":15032},"schema":{"properties":{"faces":{"description":"Face records for this page; each has id, image_url, score (0 for listings), and optional labels/age_range/ingested_at.","items":{"type":"object"},"type":"array"},"limit":{"description":"Echoed effective page size.","type":"integer"},"offset":{"description":"Echoed page offset.","type":"integer"},"total":{"description":"Total faces in the tenant gallery (for pagination).","type":"integer"}},"required":["faces","total","offset","limit"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns one page of the tenant's enrolled face gallery (id + viewer image URL) for the 'pick an existing face' grid.","tags":["Faces"]}},"/api/v1/faces/enroll":{"post":{"description":"Redeems a check_token from /faces/enroll/check against the operator's same/new decision, then drives the SAME async forensics enroll pipeline the blind /faces/ingest path uses. Returns 202 with a job to stream (kind 'face', phase 'queued'). The check_token binds the exact uploaded image, its embedding, and the candidates shown, so a decision can never be handed a different image or claim an unseen candidate.","requestBody":{"content":{"application/json":{"example":{"age_range":"30-40","case_id":"2026-0042","check_token":"a1b2c3d4e5f6","decision":"same:vgg-n000029-img14","id":"case-2026-0042","labels":["suspect"],"notes":"booking photo","subject":"John Doe"},"schema":{"properties":{"age_range":{"description":"Optional enrollment age band, for cross-age grouping.","type":"string"},"case_id":{"description":"Optional case identifier stored in vector metadata.","type":"string"},"check_token":{"description":"Opaque token minted by /faces/enroll/check; bad/expired/foreign token 400s.","type":"string"},"decision":{"description":"'new' (enroll as a new identity) or 'same:\u003ccandidate_id\u003e' (link to a shown candidate).","type":"string"},"id":{"description":"Subject/case id to enroll under; empty 400s.","type":"string"},"labels":{"description":"Optional labels attached to the enrolled face vector.","items":{"type":"string"},"type":"array"},"notes":{"description":"Optional free-text notes stored in metadata.","type":"string"},"subject":{"description":"Optional subject name/identifier stored in metadata.","type":"string"}},"required":["check_token","decision","id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"decision":"same","job_id":"job_7f3a2b1c","kind":"face","matched_candidate_id":"vgg-n000029-img14","phase":"queued","record_id":"case-2026-0042"},"schema":{"properties":{"decision":{"description":"Resolved decision kind ('new' or 'same').","type":"string"},"job_id":{"description":"Async enrollment job id to poll/stream via /api/v1/forensics/jobs/{id}.","type":"string"},"kind":{"description":"Always 'face'.","type":"string"},"matched_candidate_id":{"description":"The linked candidate id; present only for a 'same' decision.","type":"string"},"phase":{"description":"Initial phase, always 'queued'.","type":"string"},"record_id":{"description":"The subject/case id being enrolled (echoes request id).","type":"string"}},"required":["job_id","record_id","kind","phase","decision"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Redeems a check_token from /faces/enroll/check against the operator's same/new decision, then drives the SAME async forensics enroll pipeline the blind /faces/ingest path uses.","tags":["Faces"]}},"/api/v1/faces/enroll/check":{"post":{"description":"Runs the guided detect -\u003e recognize -\u003e classify gate on an uploaded image and mints the check_token the enroll decision is bound to (#542). Returns the ranked existing candidates, a recommendation band (no_match/review/strong_match), and the token; does NOT enroll anything.","requestBody":{"content":{"application/json":{"example":{"data":"iVBORw0KGgoAAAANSUhEUgAA...","k":12},"schema":{"properties":{"data":{"description":"Base64 PNG/JPEG (or data URL) of the query face; invalid image 400s, no detectable face 422s.","type":"string"},"k":{"description":"Number of candidates to rank; defaults to the server default search K when \u003c= 0.","type":"integer"}},"required":["data"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"candidates":[{"age_range":"30-40","id":"vgg-n000029-img14","image_url":"/api/v1/faces/vgg-n000029-img14/image","labels":["vgg-n000029"],"score":0.7519}],"check_token":"a1b2c3d4e5f6","faces_detected":1,"recommendation":"strong_match"},"schema":{"properties":{"candidates":{"description":"Ranked existing gallery matches (facematch.Record: id, image_url, score, labels, age_range).","items":{"type":"object"},"type":"array"},"check_token":{"description":"Opaque token to pass to POST /faces/enroll; binds the image + embedding + shown candidates.","type":"string"},"faces_detected":{"description":"Detected face count when the embedder reports it; omitted otherwise (pointer).","type":"integer"},"recommendation":{"description":"'no_match' | 'review' | 'strong_match' — classification band for the top candidate.","type":"string"}},"required":["candidates","recommendation","check_token"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs the guided detect -\u003e recognize -\u003e classify gate on an uploaded image and mints the check_token the enroll decision is bound to (#542).","tags":["Faces"]}},"/api/v1/faces/export/feedback-pairs":{"get":{"description":"Streams the tenant's entire HITL verdict corpus as JSONL (application/x-ndjson) training pairs for #530 embedder fine-tuning: one line per verdict where confirmed verdicts are positive pairs and rejected verdicts are hard negatives. Written record-by-record (never buffered whole); an empty corpus still returns a well-typed empty 200 JSONL response.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"candidate_id":{"description":"The ranked candidate face id the verdict was cast on.","type":"string"},"created_at":{"description":"RFC-3339 timestamp the verdict was recorded.","type":"string"},"query_ref":{"description":"Query side of the match (gallery face id or uploaded-image content hash).","type":"string"},"tenant":{"description":"Owning tenant slug.","type":"string"},"verdict":{"description":"'confirmed' (positive pair) or 'rejected' (hard negative).","type":"string"}},"required":["query_ref","candidate_id","verdict","created_at","tenant"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Streams the tenant's entire HITL verdict corpus as JSONL (application/x-ndjson) training pairs for #530 embedder fine-tuning: one line per verdict where confirmed verdicts are positive pairs and rejected verdicts are hard negatives.","tags":["Faces"],"x-stability":"experimental"}},"/api/v1/faces/feedback":{"get":{"description":"Returns the tenant's stored HITL match verdicts (confirm/reject records), newest first, optionally filtered by candidate_id and/or query_ref. Paged with the shared gallery pager (default 48, cap 200).","requestBody":{"content":{"application/json":{"schema":{"properties":{"candidate_id":{"description":"Filter to verdicts cast on this candidate face id.","type":"string"},"limit":{"description":"Page size query param; default 48, cap 200.","type":"integer"},"offset":{"description":"Zero-based page offset; default 0.","type":"integer"},"query_ref":{"description":"Filter to verdicts from this query (gallery id or uploaded-image hash).","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"feedback":[{"candidate_id":"vgg-n000029-img16","created_at":"2026-07-12T14:55:32Z","created_by":"anonymous","id":"780038eecb183c42","query_ref":"vgg-n000029-img00","tenant":"demo","verdict":"confirmed"},{"candidate_id":"vgg-n000029-img12","created_at":"2026-07-12T14:55:31Z","created_by":"anonymous","id":"94395f01b0608efc","query_ref":"vgg-n000029-img00","tenant":"demo","verdict":"confirmed"}],"limit":2,"offset":0,"total":1220},"schema":{"properties":{"feedback":{"description":"MatchFeedback records: id, tenant, query_ref, candidate_id, verdict, optional notes, created_by, created_at.","items":{"type":"object"},"type":"array"},"limit":{"description":"Echoed effective page size.","type":"integer"},"offset":{"description":"Echoed page offset.","type":"integer"},"total":{"description":"Total matching verdicts (for pagination).","type":"integer"}},"required":["feedback","total","offset","limit"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the tenant's stored HITL match verdicts (confirm/reject records), newest first, optionally filtered by candidate_id and/or query_ref.","tags":["Faces"]},"post":{"description":"Records one HITL confirm/reject verdict on a ranked search candidate so the #530 accuracy loop has ground truth to train against. verdict must be 'confirmed' or 'rejected' (case-insensitive); anything else 400s. Returns the persisted MatchFeedback record (201 Created).","requestBody":{"content":{"application/json":{"example":{"candidate_id":"vgg-n000029-img16","notes":"same subject, older photo","query_ref":"vgg-n000029-img00","verdict":"confirmed"},"schema":{"properties":{"candidate_id":{"description":"The ranked candidate face id the verdict is cast on; required.","type":"string"},"notes":{"description":"Optional free-text note, capped defensively server-side.","type":"string"},"query_ref":{"description":"Query side of the match — a gallery face id or an uploaded-image content hash; required.","type":"string"},"verdict":{"description":"'confirmed' or 'rejected' (lowercased/trimmed server-side).","type":"string"}},"required":["query_ref","candidate_id","verdict"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"candidate_id":"vgg-n000029-img16","created_at":"2026-07-12T14:55:32Z","created_by":"anonymous","id":"780038eecb183c42","notes":"same subject, older photo","query_ref":"vgg-n000029-img00","tenant":"demo","verdict":"confirmed"},"schema":{"properties":{"candidate_id":{"description":"Echoed candidate id.","type":"string"},"created_at":{"description":"RFC-3339 creation timestamp.","type":"string"},"created_by":{"description":"Identity that cast the verdict (subjectOf request; 'anonymous' on demo).","type":"string"},"id":{"description":"Generated verdict record id.","type":"string"},"notes":{"description":"Echoed notes; omitted when empty.","type":"string"},"query_ref":{"description":"Echoed query reference.","type":"string"},"tenant":{"description":"Owning tenant slug.","type":"string"},"verdict":{"description":"Normalized verdict ('confirmed'/'rejected').","type":"string"}},"required":["id","tenant","query_ref","candidate_id","verdict","created_by","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Records one HITL confirm/reject verdict on a ranked search candidate so the #530 accuracy loop has ground truth to train against.","tags":["Faces"]}},"/api/v1/faces/ingest":{"post":{"description":"Enrolls a face BLINDLY (no recognition check) through the shared async forensics engine — the bulk/seed intake path for callers that already know there is no prior art to check against. Creates an enrollment job, kicks off the out-of-band embed -\u003e store -\u003e index pipeline, and returns 202 with a job_id to stream. Enroll-with-recognition instead uses /faces/enroll/check + /faces/enroll.","requestBody":{"content":{"application/json":{"example":{"age_range":"30-40","case_id":"2026-0042","data":"iVBORw0KGgoAAAANSUhEUgAA...","id":"face-2026-0042","labels":["seed"],"notes":"seed intake","subject":"John Doe"},"schema":{"properties":{"age_range":{"description":"Optional enrollment age band (cross-age grouping).","type":"string"},"body_location":{"description":"Optional body-location tag (shared with tattoo intake; typically unused for faces).","type":"string"},"case_id":{"description":"Optional case identifier stored in vector metadata.","type":"string"},"data":{"description":"Base64 PNG/JPEG (or data URL) of the face image; invalid 400s.","type":"string"},"id":{"description":"Subject/case id to enroll under; empty 400s.","type":"string"},"labels":{"description":"Optional labels attached to the vector.","items":{"type":"string"},"type":"array"},"notes":{"description":"Optional free-text notes stored in metadata.","type":"string"},"subject":{"description":"Optional subject name/identifier stored in metadata.","type":"string"}},"required":["id","data"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"job_id":"job_7f3a2b1c","kind":"face","phase":"queued","record_id":"face-2026-0042"},"schema":{"properties":{"job_id":{"description":"Async enrollment job id to poll/stream via /api/v1/forensics/jobs/{id}.","type":"string"},"kind":{"description":"Always 'face'.","type":"string"},"phase":{"description":"Initial phase, always 'queued'.","type":"string"},"record_id":{"description":"The subject/case id being enrolled.","type":"string"}},"required":["job_id","record_id","kind","phase"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Enrolls a face BLINDLY (no recognition check) through the shared async forensics engine — the bulk/seed intake path for callers that already know there is no prior art to check against.","tags":["Faces"]}},"/api/v1/faces/projection":{"get":{"description":"Returns the projection-head (verdict-learning) status for the tenant: the currently active head (if any), every stored candidate head with its held-out eval metrics, and trained_pairs — the verdict count the active head was trained on. The face-recognition retrain Flow's accumulation gate subtracts trained_pairs from the current verdict total to decide whether enough new verdicts have arrived to retrain.","responses":{"200":{"content":{"application/json":{"example":{"active":null,"heads":null,"trained_pairs":0},"schema":{"properties":{"active":{"description":"The active ProjectionHead (version, dim, weights, status, metrics, trained_at, tenant, created_by) or null if none is active.","type":"object"},"heads":{"description":"All stored heads (candidate/active/retired) with eval metrics; null when none exist.","items":{"type":"object"},"type":"array"},"trained_pairs":{"description":"Verdict-pair count the active head was trained on (0 when no active head); top-level so the Flow binding resolves even with no active head.","type":"integer"}},"required":["active","heads","trained_pairs"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the projection-head (verdict-learning) status for the tenant: the currently active head (if any), every stored candidate head with its held-out eval metrics, and trained_pairs — the verdict count the active head was trained on.","tags":["Faces"],"x-stability":"experimental"}},"/api/v1/faces/projection/activate":{"post":{"description":"Promotes a stored candidate projection head to active (retiring the previous active), applied at search-rescore time. The retrain Flow calls this only after its gate confirms the candidate beats baseline on held-out verdicts.","requestBody":{"content":{"application/json":{"example":{"version":"20260712-1455"},"schema":{"properties":{"version":{"description":"The candidate head version to activate; empty 400s, unknown 404s.","type":"string"}},"required":["version"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"activated":true,"head":{"created_by":"service","dim":768,"metrics":{"baseline_agreement":0.83,"epochs":300,"eval_pairs":24,"pairs":120,"threshold":0.42,"train_pairs":96,"tuned_agreement":0.91},"status":"active","tenant":"demo","trained_at":"2026-07-12T14:55:00Z","version":"20260712-1455","weights":[1,1]}},"schema":{"properties":{"activated":{"description":"Always true on success.","type":"boolean"},"head":{"description":"The now-active ProjectionHead (version, dim, weights, status 'active', metrics, trained_at, tenant, created_by).","type":"object"}},"required":["activated","head"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Promotes a stored candidate projection head to active (retiring the previous active), applied at search-rescore time.","tags":["Faces"],"x-stability":"experimental"}},"/api/v1/faces/projection/train":{"post":{"description":"Trains a candidate projection head from ALL stored verdicts and returns it with held-out eval metrics; it NEVER activates (the Flow's gate + activate step decide that). Too few resolvable verdicts is a normal 'not yet' 200 ({trained:false}) rather than an error, so the Flow gate can branch to stop.","requestBody":{"content":{"application/json":{"example":{}}}},"responses":{"200":{"content":{"application/json":{"example":{"improves":true,"metrics":{"baseline_agreement":0.83,"epochs":300,"eval_pairs":24,"pairs":120,"threshold":0.42,"train_pairs":96,"tuned_agreement":0.91},"pairs":120,"skipped":3,"trained":true,"version":"20260712-1455"},"schema":{"properties":{"improves":{"description":"Whether the candidate beats baseline agreement on the eval split; present only when trained=true.","type":"boolean"},"metrics":{"description":"TrainMetrics (pairs, train_pairs, eval_pairs, baseline_agreement, tuned_agreement, threshold, epochs); present only when trained=true.","type":"object"},"pairs":{"description":"Verdicts that resolved into usable embedding pairs.","type":"integer"},"reason":{"description":"Why training was skipped; present only when trained=false.","type":"string"},"skipped":{"description":"Verdicts skipped because a face image is no longer stored.","type":"integer"},"trained":{"description":"Whether a candidate head was produced; false when too few verdicts resolved.","type":"boolean"},"version":{"description":"The trained candidate head version; present only when trained=true.","type":"string"}},"required":["trained","pairs","skipped"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Trains a candidate projection head from ALL stored verdicts and returns it with held-out eval metrics; it NEVER activates (the Flow's gate + activate step decide that).","tags":["Faces"],"x-stability":"experimental"}},"/api/v1/faces/reembed":{"post":{"description":"Batch re-embeds already-enrolled faces into a SECOND embedding space and a parallel target collection (#535), without touching the live search collection — the per-batch unit of work a LakeFlow backfill pipeline drives. Migrates either an explicit `ids` batch (up to 64) or, when ids is omitted, walks the gallery page-by-page via offset/limit; the response's next_offset resumes the walk (0 once exhausted).","requestBody":{"content":{"application/json":{"example":{"ids":["vgg-n000029-img14","vgg-n000029-img07"],"target_collection":"faces-demo-arc"},"schema":{"properties":{"ids":{"description":"Explicit face ids to migrate (max 64); blank entries dropped. Omit to walk the gallery instead.","items":{"type":"string"},"type":"array"},"limit":{"description":"Gallery-walk page size (ids omitted); default 100, cap 500.","type":"integer"},"offset":{"description":"Gallery-walk start offset (ids omitted); negatives clamped to 0.","type":"integer"},"target_collection":{"description":"Destination collection name; must match ^[A-Za-z0-9_-]+(\\.[A-Za-z0-9_-]+)*$, \u003c=200 chars, and must NOT be the active FACE_COLLECTION.","type":"string"}},"required":["target_collection"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"failed":[],"next_offset":0,"processed":2},"schema":{"properties":{"failed":{"description":"Face ids that failed (bad image/embed/upsert); a single failure never fails the batch.","items":{"type":"string"},"type":"array"},"next_offset":{"description":"Resume offset for the gallery walk; 0 once the gallery is exhausted (and for explicit-ids batches).","type":"integer"},"processed":{"description":"Number of faces successfully re-embedded and upserted this batch.","type":"integer"}},"required":["processed","failed","next_offset"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Batch re-embeds already-enrolled faces into a SECOND embedding space and a parallel target collection (#535), without touching the live search collection — the per-batch unit of work a LakeFlow backfill pipeline drives.","tags":["Faces"],"x-stability":"experimental"}},"/api/v1/faces/search":{"post":{"description":"Ranks the tenant's gallery against a query that is EITHER an existing face `id` or an uploaded base64 image (`data`), returning hits sorted by cosine similarity (0-1, highest first) with the accept/review bands the UI groups them by. Stored HITL verdicts for this query are folded in (best-effort): rejected candidates carry hitl='rejected' and sink, confirmed carry hitl='confirmed' and win ties; raw scores are never altered and a feedback fault never fails the search.","requestBody":{"content":{"application/json":{"example":{"id":"vgg-n000029-img00","k":3},"schema":{"properties":{"data":{"description":"Base64 PNG/JPEG (or data URL) query image; invalid image 400s. One of id/data required.","type":"string"},"id":{"description":"Existing gallery face id to query by; mutually exclusive with data. One of id/data required.","type":"string"},"k":{"description":"Number of nearest neighbors to return; defaults to the server default search K when \u003c= 0.","type":"integer"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"bands":{"accept":0.5,"review":0.3},"hits":[{"hitl":"confirmed","id":"vgg-n000029-img14","image_key":"faces/vgg-n000029-img14.png","image_url":"/api/v1/faces/vgg-n000029-img14/image","labels":["vgg-n000029"],"score":0.751943},{"hitl":"confirmed","id":"vgg-n000029-img07","image_key":"faces/vgg-n000029-img07.png","image_url":"/api/v1/faces/vgg-n000029-img07/image","labels":["vgg-n000029"],"score":0.73776245},{"hitl":"confirmed","id":"vgg-n000029-img10","image_key":"faces/vgg-n000029-img10.png","image_url":"/api/v1/faces/vgg-n000029-img10/image","labels":["vgg-n000029"],"score":0.7243756}]},"schema":{"properties":{"bands":{"description":"Classification thresholds {accept, review} the UI uses to group hits into strong/review/weak.","type":"object"},"hits":{"description":"Ranked facematch.Record results: id, image_url, image_key, score (0-1), labels, optional age_range, optional hitl verdict.","items":{"type":"object"},"type":"array"}},"required":["hits","bands"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Ranks the tenant's gallery against a query that is EITHER an existing face `id` or an uploaded base64 image (`data`), returning hits sorted by cosine similarity (0-1, highest first) with the accept/review bands the UI groups them by.","tags":["Faces"]}},"/api/v1/faces/search/stream":{"post":{"description":"Same query contract as POST /faces/search but streamed as Server-Sent Events (text/event-stream) so the UI can show the live pipeline (#664): emits phase frames received -\u003e embedding -\u003e searching -\u003e ranking, each BEFORE its stage runs, then a terminal 'done' frame carrying the hits, query_ref, and bands (or a 'failed' frame with an error).","requestBody":{"content":{"application/json":{"example":{"id":"vgg-n000029-img00","k":2},"schema":{"properties":{"data":{"description":"Base64 PNG/JPEG (or data URL) query image; one of id/data required.","type":"string"},"id":{"description":"Existing gallery face id to query by; one of id/data required.","type":"string"},"k":{"description":"Number of nearest neighbors; defaults to the server default search K when \u003c= 0.","type":"integer"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"bands":{"description":"{accept, review} thresholds; on the 'done' frame.","type":"object"},"error":{"description":"Error message; on a 'failed' frame, alongside a status field.","type":"string"},"hits":{"description":"Ranked facematch.Record hits; present on the terminal 'done' frame.","items":{"type":"object"},"type":"array"},"phase":{"description":"Per-frame stage: 'received' | 'embedding' | 'searching' | 'ranking' | 'done' | 'failed'.","type":"string"},"query_ref":{"description":"The feedback identity of this query (gallery id or uploaded-image hash); on the 'done' frame.","type":"string"},"status":{"description":"HTTP-equivalent status; on a 'failed' frame.","type":"integer"}},"required":["phase"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Same query contract as POST /faces/search but streamed as Server-Sent Events (text/event-stream) so the UI can show the live pipeline (#664): emits phase frames received -\u003e embedding -\u003e searching -\u003e ranking, each BEFORE its stage runs, then a terminal 'done' frame carrying the hits, query_ref, and bands (or a 'failed' frame with an error).","tags":["Faces"]}},"/api/v1/faces/{id}/image":{"get":{"description":"Serves one enrolled face's raw PNG bytes (Content-Type image/png) for the viewer and grid thumbnails. The id comes from the path (falls back to an `id` query param); 404 if no image exists for that id.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Face id — path segment (or `id` query param fallback).","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"(binary)":{"description":"Raw PNG image bytes; response is image/png, not JSON.","type":"string"}},"required":["(binary)"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Serves one enrolled face's raw PNG bytes (Content-Type image/png) for the viewer and grid thumbnails.","tags":["Faces"]}},"/api/v1/forensics/jobs/{id}":{"get":{"description":"Polls one async biometric-enrollment job by its id and returns the job's current observable state (phase, percent, message, optional image URL/error, timestamps). This is the SSE fallback for clients without EventSource — it reads the in-memory Manager's job registry and returns a snapshot of the same progress fields the stream endpoint pushes.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path segment {id}: the job id returned by a prior enroll call (202 {job_id}). Required by the route; an unknown id yields 404.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-15T12:00:00Z","image_url":"/api/v1/faces/case-4412/image","job_id":"7f3c1a9e2b","kind":"face","message":"Enrollment complete — added to the gallery","pct":100,"phase":"done","record_id":"case-4412","updated_at":"2026-07-15T12:00:03Z"},"schema":{"properties":{"created_at":{"description":"RFC3339 UTC timestamp when the job was registered.","type":"string"},"error":{"description":"Failure reason; present (omitempty) only when phase is failed.","type":"string"},"image_url":{"description":"URL to the enrolled image once stored; omitted (omitempty) until available.","type":"string"},"job_id":{"description":"The job's unique id (Job.ID).","type":"string"},"kind":{"description":"Biometric kind being enrolled: \"tattoo\" or \"face\".","type":"string"},"message":{"description":"Human-readable status line for the current phase.","type":"string"},"pct":{"description":"Progress percentage 0-100 for the UI meter (queued=5, embedding=30, stored=60, indexed=85, done/failed=100).","type":"integer"},"phase":{"description":"Current lifecycle phase: queued | embedding | stored | indexed | done | failed.","type":"string"},"record_id":{"description":"The subject/case id the enrollment was submitted under.","type":"string"},"updated_at":{"description":"RFC3339 UTC timestamp of the last phase transition.","type":"string"}},"required":["job_id","kind","record_id","phase","pct","message","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Polls one async biometric-enrollment job by its id and returns the job's current observable state (phase, percent, message, optional image URL/error, timestamps).","tags":["Forensics"]}},"/api/v1/forensics/jobs/{id}/stream":{"get":{"description":"Streams an enrollment job's live progress as Server-Sent Events (Content-Type text/event-stream), emitting one frame per phase transition and terminating when the job reaches a terminal phase (done or failed). It writes an immediate initial frame with the current phase so a late subscriber renders at once, then relays each subsequent Progress update from the Manager; it is reconnect-tolerant, so reconnecting after completion delivers the terminal frame and closes.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path segment {id}: the job id to subscribe to. Required; an unknown id yields 404.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"error":{"description":"SSE data field: failure reason; present (omitempty) only on the failed frame.","type":"string"},"image_url":{"description":"SSE data field: enrolled image URL; omitted (omitempty) until stored.","type":"string"},"job_id":{"description":"SSE data field: the job's id (Progress.JobID).","type":"string"},"kind":{"description":"SSE data field: \"tattoo\" or \"face\".","type":"string"},"message":{"description":"SSE data field: human-readable status for the phase.","type":"string"},"pct":{"description":"SSE data field: progress percentage 0-100.","type":"integer"},"phase":{"description":"SSE data field and the SSE event: name; the phase this frame reports (queued|embedding|stored|indexed|done|failed).","type":"string"},"record_id":{"description":"SSE data field: subject/case id; omitted (omitempty) when empty.","type":"string"}},"required":["job_id","kind","phase","pct","message"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Streams an enrollment job's live progress as Server-Sent Events (Content-Type text/event-stream), emitting one frame per phase transition and terminating when the job reaches a terminal phase (done or failed).","tags":["Forensics"],"x-stability":"experimental"}},"/api/v1/forensics/pipeline":{"get":{"description":"Returns the read-only definition of the proprietary pixelMapper biometric matching pipeline so the UI can render it as a view-only diagram the tenant watches execute. The payload is a static, hardcoded description of the five happy-path phases (queued, embedding, stored, indexed, done); there is deliberately no mutating counterpart because the flow, algorithm, and thresholds are platform IP and are not tenant-editable.","requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"No request body and no query params; this is a static GET.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"editable":false,"name":"pixelMapper biometric matching","owner":"platform","phases":[{"desc":"Enrollment accepted; awaiting the async worker.","label":"Queued","phase":"queued"},{"desc":"Vision-language image embedding (embed-server).","label":"Embedding","phase":"embedding"},{"desc":"Image bytes persisted to the tenant object store.","label":"Stored","phase":"stored"},{"desc":"Vector upserted into the binary-quantized index.","label":"Indexed","phase":"indexed"},{"desc":"Searchable in the gallery.","label":"Done","phase":"done"}]},"schema":{"properties":{"editable":{"description":"Always false — the tenant may watch the flow execute but cannot edit the definition.","type":"boolean"},"name":{"description":"Human display name of the pipeline; constant \"pixelMapper biometric matching\".","type":"string"},"owner":{"description":"Owner of the flow definition; constant \"platform\" (not the tenant).","type":"string"},"phases":{"description":"Ordered list of the pipeline's phases; each element is {phase, label, desc} where phase is the machine key (queued|embedding|stored|indexed|done), label is the UI caption, and desc is the one-line explanation.","items":{"type":"object"},"type":"array"}},"required":["name","owner","editable","phases"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the read-only definition of the proprietary pixelMapper biometric matching pipeline so the UI can render it as a view-only diagram the tenant watches execute.","tags":["Forensics"]}},"/api/v1/glossary":{"get":{"description":"Lists the calling tenant's business glossary terms (governed business definitions such as KPIs and taxonomies), sorted alphabetically (case-insensitive) by name. Each term is read from a per-term JSON object under the tenant-scoped glossaryPrefix; optional query filters narrow the result to a single category or to terms linked to a specific catalog table FQN.","requestBody":{"content":{"application/json":{"schema":{"properties":{"category":{"description":"Case-insensitive exact-match filter on the term's category.","type":"string"},"table":{"description":"Return only terms whose linked tables list contains this catalog.schema.table FQN (exact match).","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"terms":[]},"schema":{"properties":{"terms":{"description":"Array of glossary term objects (id, name, definition, category, tables, created_by, created_at, updated_at), sorted by lowercased name; empty array when the tenant has no matching terms.","items":{"type":"object"},"type":"array"}},"required":["terms"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists the calling tenant's business glossary terms (governed business definitions such as KPIs and taxonomies), sorted alphabetically (case-insensitive) by name.","tags":["Glossary"]},"post":{"description":"Creates a new business glossary term for the calling tenant. Validates that name and definition are non-blank and that every linked table is a valid catalog.schema.table FQN, then persists a term object with a freshly generated 16-hex-char id, the RFC3339 UTC created/updated timestamps, and created_by set from the request subject.","requestBody":{"content":{"application/json":{"example":{"category":"Finance KPI","definition":"Normalized recurring subscription revenue for a calendar month.","name":"Monthly Recurring Revenue","tables":["analytics.finance.subscriptions"]},"schema":{"properties":{"category":{"description":"Optional grouping category (e.g. taxonomy/KPI); trimmed and stored.","type":"string"},"definition":{"description":"The business definition text; must be non-blank after trimming (400 otherwise).","type":"string"},"name":{"description":"Human-readable term name; must be non-blank after trimming (400 otherwise).","type":"string"},"tables":{"description":"Optional list of linked catalog table FQNs; each must be a valid catalog.schema.table or the request is rejected with 400.","items":{"type":"string"},"type":"array"}},"required":["name","definition"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"category":"Finance KPI","created_at":"2026-07-15T12:00:00Z","created_by":"user@example.com","definition":"Normalized recurring subscription revenue for a calendar month.","id":"a1b2c3d4e5f60718","name":"Monthly Recurring Revenue","tables":["analytics.finance.subscriptions"],"updated_at":"2026-07-15T12:00:00Z"},"schema":{"properties":{"category":{"description":"Trimmed category (omitted from JSON when empty).","type":"string"},"created_at":{"description":"RFC3339 UTC creation timestamp.","type":"string"},"created_by":{"description":"Request subject that created the term (omitted when empty).","type":"string"},"definition":{"description":"Trimmed definition text as stored.","type":"string"},"id":{"description":"Server-generated 16-char hex identifier for the term.","type":"string"},"name":{"description":"Trimmed term name as stored.","type":"string"},"tables":{"description":"Linked table FQNs as supplied (omitted from JSON when empty).","items":{"type":"string"},"type":"array"},"updated_at":{"description":"RFC3339 UTC last-update timestamp (equals created_at on create).","type":"string"}},"required":["id","name","definition","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates a new business glossary term for the calling tenant.","tags":["Glossary"]}},"/api/v1/glossary/{id}":{"delete":{"description":"Deletes the glossary term with the given path id from the tenant's object store. The delete is idempotent at the store level (deleting a non-existent id is not treated as an error), returning 204 No Content on success.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes the glossary term with the given path id from the tenant's object store.","tags":["Glossary"]},"put":{"description":"Replaces the mutable fields (name, definition, category, tables) of an existing glossary term identified by the path id, preserving id/created_by/created_at and bumping updated_at. Applies the same validation as create (non-blank name and definition, valid linked-table FQNs) and returns 404 if no term with that id exists for the tenant.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"category":"Finance KPI","definition":"Normalized recurring subscription revenue for a calendar month, net of refunds.","name":"Monthly Recurring Revenue","tables":["analytics.finance.subscriptions"]},"schema":{"properties":{"category":{"description":"New category (trimmed); replaces the prior value, empty clears it.","type":"string"},"definition":{"description":"New definition text; must be non-blank after trimming.","type":"string"},"name":{"description":"New term name; must be non-blank after trimming.","type":"string"},"tables":{"description":"New linked table FQN list; fully replaces the prior list and each must be a valid catalog.schema.table.","items":{"type":"string"},"type":"array"}},"required":["name","definition"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"category":"Finance KPI","created_at":"2026-07-14T09:00:00Z","created_by":"user@example.com","definition":"Normalized recurring subscription revenue for a calendar month, net of refunds.","id":"a1b2c3d4e5f60718","name":"Monthly Recurring Revenue","tables":["analytics.finance.subscriptions"],"updated_at":"2026-07-15T12:00:00Z"},"schema":{"properties":{"category":{"description":"Updated category (omitted when empty).","type":"string"},"created_at":{"description":"Original creation timestamp, preserved.","type":"string"},"created_by":{"description":"Original creator subject, preserved (omitted when empty).","type":"string"},"definition":{"description":"Updated trimmed definition.","type":"string"},"id":{"description":"The term id (unchanged from the path).","type":"string"},"name":{"description":"Updated trimmed name.","type":"string"},"tables":{"description":"Updated linked table FQN list (omitted when empty).","items":{"type":"string"},"type":"array"},"updated_at":{"description":"RFC3339 UTC timestamp of this update.","type":"string"}},"required":["id","name","definition","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Replaces the mutable fields (name, definition, category, tables) of an existing glossary term identified by the path id, preserving id/created_by/created_at and bumping updated_at.","tags":["Glossary"]}},"/api/v1/ingest/archive":{"post":{"description":"Accepts a multipart file upload of an archive/document (zip/tar.gz of images, a PDF, or a .dcm DICOM file), streams the first file part to a temp file, and runs the shared durable ingest pipeline: content-hash (sha256) dedup across this and prior batches, per-format dispatch (DICOM de-identify+render, PDF rasterize via pdftoppm, or zip-of-images extraction), and enqueues each unique page as an OCR/classify/summarize job for the worker tier. Returns the new batch id with page/dedup counts.","requestBody":{"content":{"application/json":{"schema":{"properties":{"collection":{"description":"Query param: optional target documents-object FQN (catalog.schema.object) the batch is scanned into.","type":"string"},"file":{"description":"multipart/form-data: the first part that has a filename is used (field name is not checked); include a zip/tar.gz/gz/pdf/dcm file.","type":"string"},"model":{"description":"Query param: optional governed model id applied to every job in the batch.","type":"string"}},"required":["file"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"batch_id":"a1b2c3d4e5f6","collection":"main.docs.reports","deduped":3,"extracted":15,"filename":"report.pdf","pages":12},"schema":{"properties":{"batch_id":{"description":"Identifier of the created ingest batch.","type":"string"},"collection":{"description":"The collection FQN the batch was assigned to (echoes the query param; empty if unset).","type":"string"},"deduped":{"description":"Number of pages skipped as duplicate content (sha256 already seen).","type":"integer"},"extracted":{"description":"Total page images yielded from the upload before dedup (the raster/extract 'pages' count).","type":"integer"},"filename":{"description":"Original uploaded filename.","type":"string"},"pages":{"description":"Number of UNIQUE pages actually enqueued (idx).","type":"integer"}},"required":["batch_id","pages","deduped","extracted","filename","collection"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Accepts a multipart file upload of an archive/document (zip/tar.gz of images, a PDF, or a .dcm DICOM file), streams the first file part to a temp file, and runs the shared durable ingest pipeline: content-hash (sha256) dedup across this and prior batches, per-format dispatch (DICOM de-identify+render, PDF rasterize via pdftoppm, or zip-of-images extraction), and enqueues each unique page as an OCR/classify/summarize job for the worker tier.","tags":["Ingest"]}},"/api/v1/ingest/archive-url":{"post":{"description":"Server-side 'pass by reference' counterpart to /ingest/archive (#552): fetches a PDF by URL with an SSRF-hardened HTTP client (only public IPs dialed, re-validated on every redirect hop; ~100 MiB cap; must sniff as a real PDF via the %PDF- magic bytes), writes it to a temp file, and drives the exact same rasterize-\u003ejob-queue-\u003eOCR/classify/summarize-\u003egold pipeline as the multipart endpoint. Exists because Flow's HTTP connector cannot build multipart requests and its JSON step persistence corrupts forwarded binary bytes.","requestBody":{"content":{"application/json":{"example":{"category":"main.docs.reports","title":"Annual Report 2025","url":"https://www.example.gov/reports/annual-2025.pdf"},"schema":{"properties":{"category":{"description":"Optional target documents-object collection FQN (same role as /ingest/archive's ?collection=).","type":"string"},"model":{"description":"Query param: optional governed model id for the batch (mirrors /ingest/archive).","type":"string"},"title":{"description":"Optional document name; used for the object-key stem and returned filename instead of the URL's last path segment (sanitized to a bare stem).","type":"string"},"url":{"description":"Absolute http(s) URL of the PDF to fetch; validated as a public-IP target by the SSRF guard.","type":"string"}},"required":["url"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"batch_id":"9f8e7d6c5b4a","collection":"main.docs.reports","deduped":0,"extracted":42,"filename":"Annual Report 2025.pdf","pages":42},"schema":{"properties":{"batch_id":{"description":"Identifier of the created ingest batch.","type":"string"},"collection":{"description":"The collection FQN assigned (echoes 'category'; empty if unset).","type":"string"},"deduped":{"description":"Number of pages skipped as duplicate content.","type":"integer"},"extracted":{"description":"Total page images yielded before dedup.","type":"integer"},"filename":{"description":"Derived filename (title stem or URL basename, sanitized, + .pdf).","type":"string"},"pages":{"description":"Number of UNIQUE pages actually enqueued.","type":"integer"}},"required":["batch_id","pages","deduped","extracted","filename","collection"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Server-side 'pass by reference' counterpart to /ingest/archive (#552): fetches a PDF by URL with an SSRF-hardened HTTP client (only public IPs dialed, re-validated on every redirect hop; ~100 MiB cap; must sniff as a real PDF via the %PDF- magic bytes), writes it to a temp file, and drives the exact same rasterize-\u003ejob-queue-\u003eOCR/classify/summarize-\u003egold pipeline as the multipart endpoint.","tags":["Ingest"]}},"/api/v1/ingest/data":{"post":{"description":"Ingests an inline CSV string as rows into the destination gold table (catalog.schema.table) via the Data service's IngestCSV, then best-effort registers the table in the catalog and durable namespace registry so it becomes visible in the catalog tree, invalidates the cached catalog DAG, and records a usage event. Returns the number of rows written.","requestBody":{"content":{"application/json":{"example":{"CSV":"id,name\n1,alice\n2,bob","Catalog":"main","Schema":"public","Table":"events"},"schema":{"properties":{"CSV":{"description":"Raw CSV payload (header row + data rows) to ingest as table rows.","type":"string"},"Catalog":{"description":"Destination catalog name (Go field name is the JSON key — there are no json tags).","type":"string"},"Schema":{"description":"Destination schema name.","type":"string"},"Table":{"description":"Destination table name; created/registered if absent.","type":"string"}},"required":["Catalog","Schema","Table","CSV"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"rows":2},"schema":{"properties":{"rows":{"description":"Number of CSV rows written to the table.","type":"integer"}},"required":["rows"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Ingests an inline CSV string as rows into the destination gold table (catalog.schema.table) via the Data service's IngestCSV, then best-effort registers the table in the catalog and durable namespace registry so it becomes visible in the catalog tree, invalidates the cached catalog DAG, and records a usage event.","tags":["Ingest"]}},"/api/v1/ingest/destinations":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Ingest"],"x-stability":"experimental"}},"/api/v1/ingest/document":{"post":{"description":"Feeds a single document payload into the document-intelligence (IDP) service, which creates a Document record under the given batch, keyed by content hash for dedup/idempotency. The handler passes the request 'data' string through as raw bytes (a scaffold that treats text/base64 alike) and returns the new document's id and classified type.","requestBody":{"content":{"application/json":{"example":{"batch_id":"batch-2026-001","data":"JVBERi0xLjQKJcfsj6IK...","kind":"pdf"},"schema":{"properties":{"batch_id":{"description":"Batch this document belongs to; groups documents for the OAIS SIP-\u003eAIP-\u003eDIP lifecycle.","type":"string"},"data":{"description":"Raw document payload; in this scaffold it is passed straight through as bytes ([]byte(data)).","type":"string"},"kind":{"description":"Document input shape (DocKind): one of pdf, tiff, jp2, jpeg, png.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"doc_id":"doc-8f3a12","doc_type":"invoice"},"schema":{"properties":{"doc_id":{"description":"Identifier of the created Document record.","type":"string"},"doc_type":{"description":"Document type assigned by the classify stage (empty until classified).","type":"string"}},"required":["doc_id","doc_type"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Feeds a single document payload into the document-intelligence (IDP) service, which creates a Document record under the given batch, keyed by content hash for dedup/idempotency.","tags":["Ingest"],"x-stability":"experimental"}},"/api/v1/ingest/field-mappings/apply":{"post":{"description":"Stateless ingest-time field-mapping transform (#221): applies a list of pre-load mappings (irreversible salted hash, rename, drop, redact, or drop-row-if row filter) to a batch of string-keyed rows and returns the transformed/filtered rows plus in/out/dropped counts. Row-level drop_row_if filters run first so a matching row's sensitive values never get transformed or returned. Parity with Airbyte Mappings / Fivetran column hashing and row filtering; distinct from query-time masking.","requestBody":{"content":{"application/json":{"example":{"mappings":[{"field":"ssn","op":"hash","salt":"s3cr3t"},{"equals":"deleted","field":"status","op":"drop_row_if"}],"rows":[{"ssn":"123-45-6789","status":"active"},{"ssn":"987-65-4321","status":"deleted"}]},"schema":{"properties":{"mappings":{"description":"Ordered list of mappings; at least one required. Each: {op, field, to?, salt?, equals?, regex?}.","items":{"type":"object"},"type":"array"},"mappings[].equals":{"description":"Exact-match value for op=drop_row_if (equals or regex required for that op).","type":"string"},"mappings[].field":{"description":"Field the mapping targets (required for every op).","type":"string"},"mappings[].op":{"description":"Operation: hash | rename | drop | redact | drop_row_if.","type":"string"},"mappings[].regex":{"description":"Regex match for op=drop_row_if; must compile.","type":"string"},"mappings[].salt":{"description":"Salt for op=hash (recommended); hash is hex(sha256(salt+value)), deterministic + irreversible.","type":"string"},"mappings[].to":{"description":"Rename target field name (required when op=rename).","type":"string"},"rows":{"description":"Batch of rows (string-\u003estring maps) to transform; capped at 100000.","items":{"type":"string"},"type":"array"}},"required":["mappings","mappings[].op","mappings[].field"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"rows":[{"ssn":"a2f5e...c91","status":"active"}],"rows_dropped":1,"rows_in":2,"rows_out":1},"schema":{"properties":{"rows":{"description":"The surviving, transformed rows.","items":{"type":"string"},"type":"array"},"rows_dropped":{"description":"Number of rows removed by drop_row_if filters.","type":"integer"},"rows_in":{"description":"Number of rows received.","type":"integer"},"rows_out":{"description":"Number of rows returned after filtering.","type":"integer"}},"required":["rows","rows_in","rows_out","rows_dropped"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Stateless ingest-time field-mapping transform (#221): applies a list of pre-load mappings (irreversible salted hash, rename, drop, redact, or drop-row-if row filter) to a batch of string-keyed rows and returns the transformed/filtered rows plus in/out/dropped counts.","tags":["Ingest"]}},"/api/v1/ingest/object-event":{"post":{"requestBody":{"content":{"application/json":{"example":{"bucket":"vforce-lakehouse","key":"intake/acme/scan.png","tenant":"acme"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Object-storage notification intake (event-driven ingestion): accepts MinIO/S3 bucket-notification JSON (Records[].s3, s3:ObjectCreated:*) or the minimal {tenant,key,bucket} form. Objects dropped under intake/\u003ctenant\u003e/ are fetched from the object store and queued through the async batch pipeline (poll GET /api/v1/batches/{id}). Auth: shared-secret X-Ingest-Token checked against OBJECT_EVENT_TOKEN (503 when unset). Non-matching keys are acknowledged as skipped inside the 202, never a 4xx.","tags":["Ingest"],"x-stability":"experimental"}},"/api/v1/ingest/object-event/status":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Whether event-driven object ingestion is live on this deployment: configured (OBJECT_EVENT_TOKEN set), queue_ready (job queue + object store wired), and the intake key prefix. Booleans only — the secret never leaves the process.","tags":["Ingest"],"x-stability":"experimental"}},"/api/v1/ingest/process":{"post":{"description":"Governed single-page ingest into an EXISTING, caller-accessible destination table: the handler resolves the destination in the catalog and runs the ReBAC accessibleTable check (miss on either -\u003e 404, never auto-creates), base64-decodes the inline image bytes, and runs the Microfiche pipeline's ProcessInto with the requested OCR/classify/summarize steps, landing the result as a gold row. Returns the stored document key, table FQN, detected doc type, character count, and (if produced) a summary.","requestBody":{"content":{"application/json":{"example":{"data":"iVBORw0KGgoAAAANSUhEUg...","destination":{"catalog":"main","schema":"docs","table":"scans"},"index":0,"key":"page-1","metadata":{"source":"upload"},"process":{"classify":true,"docType":"","model":"","ocr":true,"summarize":true}},"schema":{"properties":{"data":{"description":"Base64-encoded image bytes for the page to process.","type":"string"},"destination":{"description":"Target table selector {catalog, schema, table}; all three are required and must already exist and be accessible.","type":"object"},"index":{"description":"Page index within its batch/document.","type":"integer"},"key":{"description":"Object key/name for this page; defaults to 'inline' when empty.","type":"string"},"metadata":{"description":"Arbitrary key/value metadata attached to the ingested record.","type":"string"},"process":{"description":"Pipeline step toggles/params {ocr:bool, classify:bool, summarize:bool, docType:string, model:string}.","type":"object"}},"required":["destination","data"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"char_count":1284,"doc_id":"page-1","doc_type":"letter","summary":"Notice of annual filing deadline.","table":"main.docs.scans"},"schema":{"properties":{"char_count":{"description":"Number of characters extracted from the page.","type":"integer"},"doc_id":{"description":"Stored record key (rec.Key) for the processed page.","type":"string"},"doc_type":{"description":"Document type detected/assigned by the pipeline.","type":"string"},"summary":{"description":"Generated summary; omitted from the response when empty.","type":"string"},"table":{"description":"Destination table FQN (catalog.schema.table) the row landed in.","type":"string"}},"required":["doc_id","table","doc_type","char_count"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Governed single-page ingest into an EXISTING, caller-accessible destination table: the handler resolves the destination in the catalog and runs the ReBAC accessibleTable check (miss on either -\u003e 404, never auto-creates), base64-decodes the inline image bytes, and runs the Microfiche pipeline's ProcessInto with the requested OCR/classify/summarize steps, landing the result as a gold row.","tags":["Ingest"]}},"/api/v1/jobs/{id}":{"get":{"description":"Polls a single async ingest job by its id and returns the job's current lifecycle state, always echoing the job id and page key. When the job has reached the terminal 'done' state the response additionally carries the derived at-a-glance result (doc type, disposition, confidence); when it has reached 'failed' it carries the error message. This is the polling counterpart to the 202-returning async enqueue endpoints (e.g. POST /api/v1/microfiche/process/async).","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the job id returned by the async enqueue endpoint (r.PathValue(\"id\")). Not a query/body field.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"job_id":"9f3c1e2a7b8d4f60","key":"batch-2026/page-0007.jp2","result":{"confidence":0.94,"disposition":"auto-approved","doc_type":"invoice","key":"batch-2026/page-0007.jp2"},"status":"done"},"schema":{"properties":{"error":{"description":"Present ONLY when status==\"failed\". The worker's error message (job.Error).","type":"string"},"job_id":{"description":"The job's id (echoed from job.ID).","type":"string"},"key":{"description":"The page identity for the job — object-store key or an inline label (job.Key).","type":"string"},"result":{"description":"Present ONLY when status==\"done\". The JobResult: {key string, doc_type string, disposition string, confidence float64} — the at-a-glance derived outcome (full OCR text lands in the gold table, not here).","type":"object"},"status":{"description":"Lifecycle state: one of \"queued\", \"running\", \"done\", or \"failed\" (job.State).","type":"string"}},"required":["job_id","status","key"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Polls a single async ingest job by its id and returns the job's current lifecycle state, always echoing the job id and page key.","tags":["Jobs"]}},"/api/v1/lineage/backfill-edges":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Lineage"],"x-stability":"experimental"}},"/api/v1/lineage/edges":{"post":{"description":"Records a single from→to derivation edge in the catalog's lineage graph, asserting that the `to` table is derived from the `from` table via the named operation (part of the #231 lineage impact-analysis feature, DataHub/Monte-Carlo parity). Both endpoint FQNs must be valid catalog.schema.table identifiers and the subject must hold `read` on BOTH tables (fail-closed) before the edge is persisted via Catalog.RecordLineage.","requestBody":{"content":{"application/json":{"example":{"from":"main.public.orders","op":"aggregate","to":"main.public.orders_daily"},"schema":{"properties":{"from":{"description":"Source/upstream table FQN (catalog.schema.table); the derivation input. Validated as a safe SQL identifier.","type":"string"},"op":{"description":"Free-text operation/transform label describing how `to` is derived from `from` (e.g. \"select\", \"join\", \"aggregate\"); stored verbatim, no validation or enum.","type":"string"},"to":{"description":"Derived/downstream table FQN (catalog.schema.table); the derivation output. Validated as a safe SQL identifier.","type":"string"}},"required":["from","to"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"from":"main.public.orders","op":"aggregate","to":"main.public.orders_daily"},"schema":{"properties":{"from":{"description":"Echo of the recorded source table FQN.","type":"string"},"op":{"description":"Echo of the recorded operation label (empty string if omitted in the request).","type":"string"},"to":{"description":"Echo of the recorded derived table FQN.","type":"string"}},"required":["from","to","op"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Records a single from→to derivation edge in the catalog's lineage graph, asserting that the `to` table is derived from the `from` table via the named operation (part of the #231 lineage impact-analysis feature, DataHub/Monte-Carlo parity).","tags":["Lineage"]}},"/api/v1/lineage/rebuild":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Lineage"],"x-stability":"experimental"}},"/api/v1/lineage/{type}/{id}":{"get":{"parameters":[{"in":"path","name":"type","required":true,"schema":{"type":"string"}},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Lineage"],"x-stability":"experimental"}},"/api/v1/llm/settings":{"get":{"description":"Returns the calling tenant's stored LLM settings — the governed OCR model id and any bring-your-own (BYO) provider configurations. A tenant with no stored settings (e.g. the demo tenant) gets an empty object, meaning the platform default OCR model and role-default resolution apply. Provider API keys are never returned: each provider's `hasKey` is set from the k8s secret store while the key itself stays server-side.","responses":{"200":{"content":{"application/json":{"example":{"ocrModel":""},"schema":{"properties":{"ocrModel":{"description":"Governed catalog model id this tenant uses for OCR; empty string means use the platform default.","type":"string"},"providers":{"description":"BYO provider configs (omitted when empty); each has provider, baseUrl, model, and hasKey.","items":{"type":"object"},"type":"array"},"providers[].baseUrl":{"description":"Provider API base URL (non-secret).","type":"string"},"providers[].hasKey":{"description":"Read-only: whether an API key is stored for this provider in the secret store.","type":"boolean"},"providers[].model":{"description":"Wire model name sent to the provider (non-secret).","type":"string"},"providers[].provider":{"description":"Logical provider id the key is stored under: one of anthropic, glm, openai.","type":"string"}},"required":["ocrModel"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the calling tenant's stored LLM settings — the governed OCR model id and any bring-your-own (BYO) provider configurations.","tags":["Llm"]},"put":{"description":"Validates and persists the calling tenant's LLM settings. The OCR model must be an approved, vision-capable catalog model (or empty for platform default); each provider must be an allow-listed id. Any supplied provider API key is written to the tenant's k8s secret bundle and stripped from the config before it is persisted to the object store, so a key never lands in MinIO. Echoes back the stored config with `hasKey` reflecting secret-store state (never the key).","requestBody":{"content":{"application/json":{"example":{"ocrModel":"local-vlm","providers":[{"apiKey":"xxxxx","baseUrl":"https://llm-gateway.example.internal/v1","model":"provider-wire-model-name","provider":"anthropic"}]},"schema":{"properties":{"ocrModel":{"description":"Governed catalog model id for OCR; must be an approved vision-capable model, or empty to use the platform default. Validated server-side.","type":"string"},"providers":{"description":"BYO provider configurations to store/update; keys are merged into the tenant's existing secret bundle (providers not listed keep their keys).","items":{"type":"object"},"type":"array"},"providers[].apiKey":{"description":"Write-only API key; if non-empty it is moved to the secret store and stripped before persistence. Requires the secret store (else 503) only when non-empty.","type":"string"},"providers[].baseUrl":{"description":"Provider API base URL (persisted, non-secret).","type":"string"},"providers[].hasKey":{"description":"Ignored on write; the server forces it false during processing and recomputes it from the secret store for the echoed response.","type":"boolean"},"providers[].model":{"description":"Wire model name (persisted, non-secret).","type":"string"},"providers[].provider":{"description":"Provider id; must be one of anthropic, glm, openai (else 400). Trimmed before validation.","type":"string"}},"required":["providers[].provider"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"ocrModel":"local-vlm","providers":[{"baseUrl":"https://llm-gateway.example.internal/v1","hasKey":true,"model":"provider-wire-model-name","provider":"anthropic"}]},"schema":{"properties":{"ocrModel":{"description":"The stored (validated) OCR model id; empty means platform default.","type":"string"},"providers":{"description":"Echoed provider configs (omitted when empty), with apiKey stripped and hasKey reflecting stored state.","items":{"type":"object"},"type":"array"},"providers[].baseUrl":{"description":"Provider API base URL.","type":"string"},"providers[].hasKey":{"description":"Whether a key is now stored for this provider.","type":"boolean"},"providers[].model":{"description":"Wire model name.","type":"string"},"providers[].provider":{"description":"Provider id (anthropic, glm, openai).","type":"string"}},"required":["ocrModel"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Validates and persists the calling tenant's LLM settings.","tags":["Llm"]}},"/api/v1/macro-packages":{"get":{"description":"Lists every macro package stored for the caller's tenant, sorted alphabetically by name. Each package is a named, tenant-scoped, reusable set of macros (parameterized SQL snippets); the handler reads every object under the `macropackages/` prefix, unmarshals each, skips any that fail to parse or have an empty name, and returns the survivors.","responses":{"200":{"content":{"application/json":{"example":{"packages":[]},"schema":{"properties":{"packages":{"description":"Array of macro package objects, sorted ascending by name.","items":{"type":"object"},"type":"array"},"packages[].macros":{"description":"The macros in the package; each has name (string), params ([]string), body (string).","items":{"type":"object"},"type":"array"},"packages[].name":{"description":"Package name (dot-joined identifier segments).","type":"string"},"packages[].updated_at":{"description":"RFC-3339 UTC timestamp of the last write.","type":"string"},"packages[].updated_by":{"description":"Subject that last wrote the package; omitted when empty (anonymous writes).","type":"string"}},"required":["packages","packages[].name","packages[].macros","packages[].updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists every macro package stored for the caller's tenant, sorted alphabetically by name.","tags":["Macro packages"]}},"/api/v1/macro-packages/expand":{"post":{"description":"Expands a SQL template against the merged macros of one or more named packages plus caller-supplied variables, returning the final rendered SQL. It loads each referenced package (a missing one → 404), merges all their macros into a single name→macro map (later packages win on name collision), then runs the pure macro engine: {{ var }} substitutes the variable literally (never re-expanded) and {{ macro(args) }} expands the macro body recursively.","requestBody":{"content":{"application/json":{"example":{"packages":["finance"],"template":"SELECT {{ dollars(amount_cents) }} AS usd FROM orders WHERE {{ region }}","vars":{"region":"us-east"}},"schema":{"properties":{"packages":{"description":"Names of macro packages whose macros are made available to the template. Each name is validated; a valid-but-unknown name → 404. Omit/empty to expand with only vars.","items":{"type":"string"},"type":"array"},"template":{"description":"The template SQL to expand. Must be non-empty (trimmed) or 400.","type":"string"},"vars":{"description":"Variable name→value pairs. Substituted literally (as data, no re-expansion / no template injection).","type":"string"}},"required":["template"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"detail":"writes are not allowed on the public demo tenant","status":403,"title":"demo tenant is read-only","type":"about:blank"},"schema":{"properties":{"expanded":{"description":"The fully rendered template with all macros expanded and variables substituted.","type":"string"}},"required":["expanded"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Expands a SQL template against the merged macros of one or more named packages plus caller-supplied variables, returning the final rendered SQL.","tags":["Macro packages"]}},"/api/v1/macro-packages/{name}":{"delete":{"description":"Deletes the macro package named by the `{name}` path segment for the caller's tenant. Idempotent from the object store's perspective; on success it writes 204 No Content with no body.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"description":"Path parameter: package name — must match the same dot-joined identifier rule as PUT (validated before delete).","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes the macro package named by the `{name}` path segment for the caller's tenant.","tags":["Macro packages"]},"put":{"description":"Creates or replaces (full overwrite) the macro package named by the `{name}` path segment. Each macro in the body is validated (name and every param must be a Go-style identifier) before anything is persisted, so one malformed macro rejects the whole request with 400 and no partial save; on success it stamps updated_by/updated_at and stores the package as a JSON object.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"macros":[{"body":"ROUND({{ col }} / 100.0, 2)","name":"dollars","params":["col"]},{"body":"status = 'active'","name":"active_only","params":[]}]},"schema":{"properties":{"macros":{"description":"The macros to store. Each macro: name (string, required, must be an identifier), params ([]string, each must be an identifier), body (string, the parameterized snippet). An empty/null array is accepted and stores a package with no macros.","items":{"type":"object"},"type":"array"},"name":{"description":"Path parameter: package name — must match dot-joined identifier segments [A-Za-z0-9_-], max 200 chars.","type":"string"}},"required":["name","macros"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"macros":[{"body":"ROUND({{ col }} / 100.0, 2)","name":"dollars","params":["col"]},{"body":"status = 'active'","name":"active_only","params":[]}],"name":"finance","updated_at":"2026-07-15T00:00:00Z","updated_by":"anonymous"},"schema":{"properties":{"macros":{"description":"The macros as stored (name, params, body).","items":{"type":"object"},"type":"array"},"name":{"description":"The stored package name (echoes the path).","type":"string"},"updated_at":{"description":"RFC-3339 UTC timestamp set at write time.","type":"string"},"updated_by":{"description":"Subject that performed this write (subjectOf); omitted when empty.","type":"string"}},"required":["name","macros","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates or replaces (full overwrite) the macro package named by the `{name}` path segment.","tags":["Macro packages"]}},"/api/v1/marketplace/listings":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Marketplace"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Marketplace"],"x-stability":"experimental"}},"/api/v1/marketplace/listings/{id}":{"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Marketplace"],"x-stability":"experimental"},"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Marketplace"],"x-stability":"experimental"}},"/api/v1/masking/class-kinds":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Masking"],"x-stability":"experimental"},"put":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Masking"],"x-stability":"experimental"}},"/api/v1/me":{"get":{"description":"Returns the caller's identity as seen by the API: the gateway-stamped email (from the oauth2-proxy X-Auth-Request-Email header), whether that email is on the platform-admin allowlist, and the tenant the request resolves to. The UI uses platformAdmin to decide whether to show the tenant switcher.","requestBody":{"content":{"application/json":{"schema":{"properties":{"X-Auth-Request-Email":{"description":"Gateway/oauth2-proxy-stamped caller email header; echoed as `email` and checked against the platform-admin allowlist. Absent for anonymous callers.","type":"string"},"X-Tenant-Id":{"description":"Explicit tenant override header; honored only for service calls (no email) or platform admins, otherwise ignored (see tenantOf resolution).","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"email":"","platformAdmin":false,"tenant":"demo"},"schema":{"properties":{"email":{"description":"Trimmed value of the X-Auth-Request-Email header; empty string for anonymous callers.","type":"string"},"platformAdmin":{"description":"True when the caller email is non-empty and on PLATFORM_ADMIN_EMAILS (defaults to eric.diana@edgentllc.com); gates tenant switching.","type":"boolean"},"tenant":{"description":"The resolved tenant id: X-Tenant-Id override (admin/service only), else the host label left of `lakehouse`, else the shared default tenant.","type":"string"}},"required":["email","platformAdmin","tenant"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the caller's identity as seen by the API: the gateway-stamped email (from the oauth2-proxy X-Auth-Request-Email header), whether that email is on the platform-admin allowlist, and the tenant the request resolves to.","tags":["Identity"]}},"/api/v1/metadata-policies/evaluate":{"post":{"description":"Runs a stateless, pure policy-as-code evaluation (issue #230, DataHub Metadata-Tests parity) of a set of metadata policies over a set of caller-supplied asset-metadata facts. Each policy optionally SELECTS the assets it applies to (by tag or by a set attribute) and then ASSERTS a pass/fail condition — required attribute present, attribute value within an allow-list, required tag present, or every PII column masked — returning a deterministically ordered report whose `passed` is false iff any error-severity policy is violated, so CI can gate on it.","requestBody":{"content":{"application/json":{"example":{"assets":[{"attributes":{"certification":"gold","owner":"data-team"},"fqn":"warehouse.public.customers","masked_columns":["email"],"pii_columns":["email","ssn"],"tags":["published"]},{"attributes":{},"fqn":"warehouse.public.orders","tags":["published"]}],"policies":[{"attribute":"owner","id":"require-owner","severity":"error","type":"require_attribute"},{"allowed":["certified","gold"],"attribute":"certification","id":"cert-approved","select_tag":"published","severity":"warning","type":"attribute_in"},{"id":"no-unmasked-pii","severity":"error","type":"no_unmasked_pii"}]},"schema":{"properties":{"assets":{"description":"The asset-metadata facts to evaluate against; may be empty/absent. Rejected with 413 if the batch exceeds 100,000 assets.","items":{"type":"object"},"type":"array"},"assets[].attributes":{"description":"Scalar metadata facts (e.g. owner, description, certification, domain) checked by require_attribute / attribute_in and by select_attribute.","type":"string"},"assets[].fqn":{"description":"Fully-qualified asset name; echoed into violation.asset_fqn and used as the primary sort key of the report.","type":"string"},"assets[].masked_columns":{"description":"Column names that are masked; used by no_unmasked_pii to determine which PII columns are covered.","items":{"type":"string"},"type":"array"},"assets[].pii_columns":{"description":"Column names flagged as PII; the no_unmasked_pii assertion fails for any of these not present in masked_columns.","items":{"type":"string"},"type":"array"},"assets[].tags":{"description":"List of tags on the asset, checked by require_tag and by select_tag.","items":{"type":"string"},"type":"array"},"policies":{"description":"The metadata policies (tests) to evaluate; must contain at least one, and each is validated (400 on any malformed policy).","items":{"type":"object"},"type":"array"},"policies[].allowed":{"description":"Allow-list of permitted values. Required (non-empty) for type attribute_in; ignored otherwise.","items":{"type":"string"},"type":"array"},"policies[].attribute":{"description":"Attribute name the assertion targets. Required (non-empty) for type require_attribute and attribute_in; ignored otherwise.","type":"string"},"policies[].id":{"description":"Policy identifier; must be non-empty (trimmed). Echoed into each violation's policy_id.","type":"string"},"policies[].select_attribute":{"description":"Selection filter: apply the policy only to assets whose named attribute is set (non-empty). Empty = all assets.","type":"string"},"policies[].select_tag":{"description":"Selection filter: apply the policy only to assets carrying this tag. Empty = all assets.","type":"string"},"policies[].severity":{"description":"Violation severity: one of info, warning, error. Only error-severity violations flip the report's passed to false.","type":"string"},"policies[].tag":{"description":"Tag that must be present on the asset. Required (non-empty) for type require_tag; ignored otherwise.","type":"string"},"policies[].type":{"description":"Assertion kind: one of require_attribute, attribute_in, require_tag, no_unmasked_pii. Unknown type → 400.","type":"string"}},"required":["policies","policies[].id","policies[].type","policies[].severity"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"assets_total":2,"evaluations":6,"passed":false,"violations":[{"asset_fqn":"warehouse.public.customers","message":"warehouse.public.customers has unmasked PII columns: ssn","policy_id":"no-unmasked-pii","severity":"error"},{"asset_fqn":"warehouse.public.orders","message":"warehouse.public.orders \"certification\"=\"\" is not allowed","policy_id":"cert-approved","severity":"warning"},{"asset_fqn":"warehouse.public.orders","message":"warehouse.public.orders is missing required \"owner\"","policy_id":"require-owner","severity":"error"}]},"schema":{"properties":{"assets_total":{"description":"Total number of assets supplied in the request.","type":"integer"},"evaluations":{"description":"Count of (policy, selected-asset) pairs actually checked, i.e. after selection filtering. JSON key is `evaluations` (Go field Evaluated).","type":"integer"},"passed":{"description":"True iff no error-severity violation occurred; warning/info violations do not affect it. Intended as the CI gate signal.","type":"boolean"},"violations":{"description":"Every failing (policy, asset) pair, sorted by asset_fqn then policy_id. Always a JSON array (never null); empty when nothing fails.","items":{"type":"object"},"type":"array"},"violations[].asset_fqn":{"description":"FQN of the asset that violated the policy.","type":"string"},"violations[].message":{"description":"Human-readable explanation of the failure: `\u003cfqn\u003e is missing required \"\u003cattr\u003e\"` (require_attribute), `\u003cfqn\u003e \"\u003cattr\u003e\"=\"\u003cval\u003e\" is not allowed` (attribute_in), `\u003cfqn\u003e is missing required tag \"\u003ctag\u003e\"` (require_tag), or `\u003cfqn\u003e has unmasked PII columns: \u003csorted, comma-separated list\u003e` (no_unmasked_pii).","type":"string"},"violations[].policy_id":{"description":"ID of the policy that was violated.","type":"string"},"violations[].severity":{"description":"Severity carried over from the violated policy (info | warning | error).","type":"string"}},"required":["violations","violations[].policy_id","violations[].asset_fqn","violations[].severity","violations[].message","assets_total","evaluations","passed"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs a stateless, pure policy-as-code evaluation (issue #230, DataHub Metadata-Tests parity) of a set of metadata policies over a set of caller-supplied asset-metadata facts.","tags":["Metadata policies"]}},"/api/v1/microfiche/batches":{"post":{"description":"Runs the pure batch-validation gate over a declared page manifest: it checks that the manifest declares at least one page and that the number of files actually seen matches the declared page count, returning either machine_validated or quarantined. This is only the validation stage; the I/O-heavy OCR/store stages are still TODO in this handler, so it does no ingestion.","requestBody":{"content":{"application/json":{"example":{"files_seen":3,"page_count":3},"schema":{"properties":{"files_seen":{"description":"Page image files actually observed; must equal page_count.","type":"integer"},"page_count":{"description":"Pages the manifest declares; must be \u003e 0 or the batch is quarantined.","type":"integer"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"reasons":null,"state":"machine_validated","validation_ok":true},"schema":{"properties":{"reasons":{"description":"Human-readable failure reasons (e.g. \"manifest declares no pages\", \"manifest/file mismatch: declared 3, saw 2\"); null/empty when OK.","items":{"type":"string"},"type":"array"},"state":{"description":"Resulting batch state: \"machine_validated\" when the gate passes, else \"quarantined\".","type":"string"},"validation_ok":{"description":"True when no validation reasons were raised.","type":"boolean"}},"required":["state","validation_ok","reasons"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs the pure batch-validation gate over a declared page manifest: it checks that the manifest declares at least one page and that the number of files actually seen matches the declared page count, returning either machine_validated or quarantined.","tags":["Microfiche"],"x-stability":"experimental"}},"/api/v1/microfiche/boxes/coverage":{"get":{"description":"Reports on-image-search readiness for the tenant's microfiche gold table: how many gold pages have per-word bounding-box geometry versus how many still lack it. The Tesseract word-boxer runs only in the worker tier, which backfills missing boxes on a periodic sweep, so this endpoint is purely a progress read the Search page uses to show 'X of Y pages are highlightable; the worker is filling the rest.' Excluded (soft-deleted) pages are not counted.","responses":{"200":{"content":{"application/json":{"example":{"missing":38,"total":128,"with_boxes":90},"schema":{"properties":{"missing":{"description":"Derived total - with_boxes; pages the worker still needs to box.","type":"integer"},"total":{"description":"Total non-excluded gold pages for the tenant.","type":"integer"},"with_boxes":{"description":"Count of those pages that already have word-box geometry.","type":"integer"}},"required":["total","with_boxes","missing"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Reports on-image-search readiness for the tenant's microfiche gold table: how many gold pages have per-word bounding-box geometry versus how many still lack it.","tags":["Microfiche"]}},"/api/v1/microfiche/ocr":{"post":{"description":"Runs real vision-model OCR on a single base64-encoded page image (JP2/TIFF/PNG/JPEG) and returns the extracted text plus its character count. This is the reachable surface of the pipeline's Extract stage only — it does not classify, score, or persist anything.","requestBody":{"content":{"application/json":{"example":{"data":"\u003cbase64 page image\u003e","index":0,"key":"ingest/b1/p0.jp2","model":""},"schema":{"properties":{"data":{"description":"Base64-encoded page image bytes; a non-base64 value is a 400.","type":"string"},"index":{"description":"Page index within the batch (metadata for the Page).","type":"integer"},"key":{"description":"Object-store key of the page; defaults to \"inline\" when empty.","type":"string"},"model":{"description":"Optional governed model id passed as domain.ModelID(body.Model) directly to Extract to override the default OCR model.","type":"string"}},"required":["data"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"chars":842,"text":"MEMORANDUM\nTo: All Staff\n..."},"schema":{"properties":{"chars":{"description":"len(text) — character count of the transcription.","type":"integer"},"text":{"description":"OCR transcription of the page.","type":"string"}},"required":["text","chars"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs real vision-model OCR on a single base64-encoded page image (JP2/TIFF/PNG/JPEG) and returns the extracted text plus its character count.","tags":["Microfiche"]}},"/api/v1/microfiche/pages/reprocess":{"post":{"description":"Cleanly re-runs a single already-processed page: it removes the existing gold row and its stale queued job, then enqueues a fresh ingest job (optionally with a different model or a rotation) for the worker tier, returning the new job id. Used to fix a bad extraction without leaving a duplicate row behind.","requestBody":{"content":{"application/json":{"example":{"index":0,"key":"ingest/b1/p0.jp2","model":"","rotate":90},"schema":{"properties":{"index":{"description":"Page index to stamp on the re-enqueued job.","type":"integer"},"key":{"description":"Object-store key of the page to reprocess; empty is a 400.","type":"string"},"model":{"description":"Optional override OCR model id; resolved via resolveOCRModel.","type":"string"},"rotate":{"description":"Degrees to rotate the page before OCR (fixes sideways/upside-down scans).","type":"integer"}},"required":["key"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"batch_id":"b1","job_id":"job_01HZX9...","key":"ingest/b1/p0.jp2"},"schema":{"properties":{"batch_id":{"description":"Batch id derived from the page key via batchIDFromKey (empty when the key has no batch segment).","type":"string"},"job_id":{"description":"Id of the newly enqueued reprocess job.","type":"string"},"key":{"description":"The page key echoed back.","type":"string"}},"required":["job_id","batch_id","key"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Cleanly re-runs a single already-processed page: it removes the existing gold row and its stale queued job, then enqueues a fresh ingest job (optionally with a different model or a rotation) for the worker tier, returning the new job id.","tags":["Microfiche"]}},"/api/v1/microfiche/pages/review":{"post":{"description":"The human-in-the-loop resolve action for a flagged page: a reviewer sets the page's disposition to auto_accept (clears it from the review queue), human_review (re-flag), or excluded (soft-delete that hides the page from the document list and search while preserving the row for restore). Rewrites the gold row in place synchronously, records a review-audit entry, and completes the work-management overlay item.","requestBody":{"content":{"application/json":{"example":{"disposition":"excluded","key":"ingest/b1/p0.jp2"},"schema":{"properties":{"disposition":{"description":"New disposition; must be one of auto_accept, human_review, excluded.","type":"string"},"key":{"description":"Object-store key of the page to review; empty is a 400.","type":"string"}},"required":["key","disposition"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"disposition":"excluded","key":"ingest/b1/p0.jp2"},"schema":{"properties":{"disposition":{"description":"The applied disposition.","type":"string"},"key":{"description":"The page key echoed back.","type":"string"}},"required":["key","disposition"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"The human-in-the-loop resolve action for a flagged page: a reviewer sets the page's disposition to auto_accept (clears it from the review queue), human_review (re-flag), or excluded (soft-delete that hides the page from the document list and search while preserving the row for restore).","tags":["Microfiche"]}},"/api/v1/microfiche/process":{"post":{"description":"Runs the full single-page IDP pipeline (Extract → Classify → Summarize → confidence score with the human-review gate) on a base64-encoded page image, persists a gold row into cedms.microfiche.pages, emits a page-processed CloudEvent, and returns the complete structured ProcessedPage record. Unlike POST /api/v1/ingest/process this uses a fixed pipeline and a hardcoded destination table.","requestBody":{"content":{"application/json":{"example":{"data":"\u003cbase64 page image\u003e","index":0,"key":"ingest/b1/p0.jp2","model":""},"schema":{"properties":{"data":{"description":"Base64-encoded page image bytes (JP2/TIFF/PNG/JPEG); non-base64 is a 400.","type":"string"},"index":{"description":"Page index within the batch.","type":"integer"},"key":{"description":"Object-store key of the page; defaults to \"inline\" when empty.","type":"string"},"model":{"description":"Optional governed model id passed as domain.ModelID(body.Model) directly to ProcessPage.","type":"string"}},"required":["data"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"char_count":842,"confidence":0.91,"disposition":"auto_accept","doc_type":"memo","index":0,"key":"ingest/b1/p0.jp2","master_key":"","preview_key":"preview/ingest/b1/p0.jp2.jpg","summary":"Internal memo regarding...","text":"MEMORANDUM...","tier":""},"schema":{"properties":{"boxes":{"description":"Per-word bounding boxes (port.WordBox, normalized 0-1) from the word-box pass; empty when no boxer wired (omitempty).","items":{"type":"object"},"type":"array"},"char_count":{"description":"Transcription character count.","type":"integer"},"confidence":{"description":"Overall pipeline confidence [0,1] driving the disposition gate.","type":"number"},"disposition":{"description":"Gate outcome: auto_accept, human_review, or excluded.","type":"string"},"doc_type":{"description":"Classifier-assigned document type.","type":"string"},"index":{"description":"Page index echoed from the request.","type":"integer"},"key":{"description":"Object-store key of the page.","type":"string"},"master_key":{"description":"Object key of the retained raw master; empty under the default process-and-discard model.","type":"string"},"preview_key":{"description":"Object key of the compact JPEG preview derivative (preview/\u003ckey\u003e.jpg); empty when no object store or preview failed.","type":"string"},"safety_flag":{"description":"Matched safety/crisis category (self-harm/violence/abuse); forces human review and fires an alert; empty when clear (omitempty).","type":"string"},"sha256":{"description":"Hex content hash of the raw master for cross-batch dedup (omitempty).","type":"string"},"source_ref":{"description":"Where the raw master lives at the source-of-record (omitempty; currently TODO/unpopulated).","type":"string"},"summary":{"description":"Generated summary of the page.","type":"string"},"text":{"description":"OCR transcription.","type":"string"},"tier":{"description":"Storage tier of the retained master (e.g. cold); empty when discarded.","type":"string"}},"required":["index","key","text","doc_type","summary","confidence","disposition","char_count","preview_key","master_key","tier"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs the full single-page IDP pipeline (Extract → Classify → Summarize → confidence score with the human-review gate) on a base64-encoded page image, persists a gold row into cedms.microfiche.pages, emits a page-processed CloudEvent, and returns the complete structured ProcessedPage record.","tags":["Microfiche"]}},"/api/v1/microfiche/process/async":{"post":{"description":"Enqueues a single base64-encoded page for out-of-band processing by the worker tier and returns 202 with a job id; the heavy OCR → classify → summarize → score work runs on a worker and the caller polls GET /api/v1/jobs/{id} for status and result. Use this instead of the synchronous /process for large or latency-tolerant ingestion.","requestBody":{"content":{"application/json":{"example":{"data":"\u003cbase64 page image\u003e","index":0,"key":"ingest/b1/p0.jp2","model":""},"schema":{"properties":{"data":{"description":"Base64-encoded page image bytes; non-base64 is a 400.","type":"string"},"index":{"description":"Page index within the batch.","type":"integer"},"key":{"description":"Object-store key of the page; defaults to \"inline\" when empty.","type":"string"},"model":{"description":"Optional governed OCR model id; resolved via resolveOCRModel (falls back to the tenant/default model).","type":"string"}},"required":["data"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"job_id":"job_01HZX8...","status":"queued"},"schema":{"properties":{"job_id":{"description":"Id of the enqueued ingest job; poll GET /api/v1/jobs/{id}.","type":"string"},"status":{"description":"Initial job state, always \"queued\" (domain.JobQueued).","type":"string"}},"required":["job_id","status"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Enqueues a single base64-encoded page for out-of-band processing by the worker tier and returns 202 with a job id; the heavy OCR → classify → summarize → score work runs on a worker and the caller polls GET /api/v1/jobs/{id} for status and result.","tags":["Microfiche"]}},"/api/v1/microfiche/sheet":{"post":{"description":"Assembles a microfiche SHEET from its individual frames: it accepts a multipart upload of the Internet Archive _jp2.zip (the frame images plus the Hugin .pto stitch project), decodes each frame to a cell-sized image, places it in the grid cell its .pto translation implies, composites a single sheet JPEG, and stores it. Returns the stored object key (viewable via GET /api/v1/objects/{key}/image) plus the grid dimensions. This is a viewable composite, not a true full-resolution gigapixel panorama blend.","requestBody":{"content":{"application/json":{"schema":{"properties":{"file":{"description":"Multipart file part: the IA _jp2.zip archive containing frame images and a Hugin .pto; the first file part is used (streamed to a temp file).","type":"string"}},"required":["file"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"cols":10,"filename":"box42_reel3_jp2.zip","frames":98,"rows":10,"sheet_key":"sheet/01HZXA.../composite.jpg"},"schema":{"properties":{"cols":{"description":"Number of grid columns from the parsed .pto layout.","type":"integer"},"filename":{"description":"Uploaded archive filename echoed back.","type":"string"},"frames":{"description":"Number of frames extracted and placed.","type":"integer"},"rows":{"description":"Number of grid rows from the parsed .pto layout.","type":"integer"},"sheet_key":{"description":"Object-store key of the composited sheet JPEG (sheet/\u003cid\u003e/composite.jpg).","type":"string"}},"required":["sheet_key","cols","rows","frames","filename"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Assembles a microfiche SHEET from its individual frames: it accepts a multipart upload of the Internet Archive _jp2.zip (the frame images plus the Hugin .pto stitch project), decodes each frame to a cell-sized image, places it in the grid cell its .pto translation implies, composites a single sheet JPEG, and stores it.","tags":["Microfiche"],"x-stability":"experimental"}},"/api/v1/mnemo/ask":{"post":{"description":"Powers Mnemo, the lakehouse's governed AI assistant (the right-docked 'ask your data' rail, the Databricks-Genie equivalent). It grounds the chat model on the caller's REAL readable estate (live namespaces + durable registry, ReBAC-filtered) plus the most relevant per-tenant estate-memory facts, then returns a conversational GitHub-flavored markdown answer that includes a runnable ```sql block only when the user asks to see or extract rows over a grounded table. Metadata and aggregate questions are answered directly in prose from the injected estate facts rather than forcing the user to run SQL.","requestBody":{"content":{"application/json":{"example":{"history":[{"content":"What catalogs do I have?","role":"user"},{"content":"You have the orchestration and isd catalogs.","role":"assistant"}],"question":"How many documents are in my estate, and which doc types are most common?","session_id":"mnemo-2f9c1a"},"schema":{"properties":{"history":{"description":"Optional prior conversation turns (legacy/stateless contract), each {role, content}; honored only when the referenced session is new/empty, otherwise server-loaded session turns take precedence.","items":{"type":"object"},"type":"array"},"history[].content":{"description":"The message text for that turn.","type":"string"},"history[].role":{"description":"Turn role; 'assistant' or 'mnemo' render as Mnemo, anything else renders as User in the prompt.","type":"string"},"question":{"description":"The user's natural-language question for Mnemo; trimmed, and a 400 is returned if empty.","type":"string"},"session_id":{"description":"Optional conversation/session identifier; when supplied, prior turns for the session are loaded server-side and continue the thread, and the (existing or newly minted) id is echoed back.","type":"string"}},"required":["question"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"answer":"You currently have **1,204 documents** across your estate. The most common doc types are **invoice** (612), **contract** (330), and **statement** (188).\n\nIf you'd like to see the raw rows, run:\n\n```sql\nSELECT doc_type, COUNT(*) AS n\nFROM cedms.gold.documents\nGROUP BY doc_type\nORDER BY n DESC\n```","session_id":"mnemo-2f9c1a"},"schema":{"properties":{"answer":{"description":"Mnemo's conversational markdown reply (trimmed model output), possibly containing a single ```sql fenced block.","type":"string"},"session_id":{"description":"The session id to continue the thread (existing, supplied, or freshly minted); omitted from the response only when persistence is disabled (no ObjectStore) so no id was produced.","type":"string"}},"required":["answer"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Powers Mnemo, the lakehouse's governed AI assistant (the right-docked 'ask your data' rail, the Databricks-Genie equivalent).","tags":["Mnemo"]}},"/api/v1/mnemo/flow":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mnemo"],"x-stability":"experimental"}},"/api/v1/model-contracts":{"get":{"description":"Lists every model contract stored for the caller's tenant, sorted alphabetically by name. Each contract declares the output schema a model guarantees plus its version, deprecation flag, ref() access level, and owning group/project (dbt-Mesh parity, #215).","responses":{"200":{"content":{"application/json":{"example":{"contracts":[]},"schema":{"properties":{"contracts":{"description":"Array of model-contract objects (empty array when none exist), each with name, version, deprecated, access, group, project, schema, updated_by, updated_at.","items":{"type":"object"},"type":"array"},"contracts[].access":{"description":"ref() visibility: private, protected, or public.","type":"string"},"contracts[].deprecated":{"description":"Whether the model is marked deprecated.","type":"boolean"},"contracts[].group":{"description":"Owning group; omitted when empty.","type":"string"},"contracts[].name":{"description":"Contract name (dot-joined identifier segments).","type":"string"},"contracts[].project":{"description":"Owning project; omitted when empty.","type":"string"},"contracts[].schema":{"description":"Guaranteed output schema: {columns:[{name,type}]}.","type":"object"},"contracts[].updated_at":{"description":"RFC3339 timestamp of the last write.","type":"string"},"contracts[].updated_by":{"description":"Subject that last wrote the contract; omitted when empty.","type":"string"},"contracts[].version":{"description":"Declared version string of the model contract.","type":"string"}},"required":["contracts","contracts[].name","contracts[].version","contracts[].deprecated","contracts[].access","contracts[].schema","contracts[].updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists every model contract stored for the caller's tenant, sorted alphabetically by name.","tags":["Model contracts"]}},"/api/v1/model-contracts/{name}":{"delete":{"description":"Deletes the model contract named by the {name} path segment from the tenant's object store. Idempotent at the handler level and returns 204 No Content with no body on success.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"(none)":{"description":"204 No Content; the handler writes no response body.","type":"string"}},"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes the model contract named by the {name} path segment from the tenant's object store.","tags":["Model contracts"]},"put":{"description":"Creates or updates the model contract named by the {name} path segment, persisting its declared output schema, version, deprecation flag, ref() access level, and owning group/project to the tenant's object store. Access defaults to \"public\" when omitted and is validated against private/protected/public.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"access":"protected","deprecated":false,"group":"analytics","project":"revenue","schema":{"columns":[{"name":"id","type":"int"},{"name":"name","type":"string"}]},"version":"1.2.0"},"schema":{"properties":{"access":{"description":"ref() visibility: private, protected, or public; defaults to public when empty, else must be one of those three or 400.","type":"string"},"deprecated":{"description":"Marks the model deprecated so consumers can avoid ref-ing it.","type":"boolean"},"group":{"description":"Owning group; stored trimmed. Used by private-access ref() checks.","type":"string"},"project":{"description":"Owning project; stored trimmed. Used by protected-access ref() checks.","type":"string"},"schema":{"description":"Guaranteed output schema {columns:[{name,type}]}; verify compares produced schemas against this.","type":"object"},"version":{"description":"Version string for the contract; stored trimmed.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"access":"protected","deprecated":false,"group":"analytics","name":"orders.daily","project":"revenue","schema":{"columns":[{"name":"id","type":"int"},{"name":"name","type":"string"}]},"updated_at":"2026-07-15T12:00:00Z","updated_by":"anonymous","version":"1.2.0"},"schema":{"properties":{"access":{"description":"Effective access level (defaulted to public if omitted).","type":"string"},"deprecated":{"description":"Stored deprecation flag.","type":"boolean"},"group":{"description":"Owning group; omitted from JSON when empty.","type":"string"},"name":{"description":"Contract name from the path.","type":"string"},"project":{"description":"Owning project; omitted from JSON when empty.","type":"string"},"schema":{"description":"Stored output schema {columns:[{name,type}]}.","type":"object"},"updated_at":{"description":"RFC3339 timestamp set at write time.","type":"string"},"updated_by":{"description":"Subject that wrote the contract (subjectOf); omitted when empty.","type":"string"},"version":{"description":"Stored (trimmed) version string.","type":"string"}},"required":["name","version","deprecated","access","schema","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates or updates the model contract named by the {name} path segment, persisting its declared output schema, version, deprecation flag, ref() access level, and owning group/project to the tenant's object store.","tags":["Model contracts"]}},"/api/v1/model-contracts/{name}/can-ref":{"post":{"description":"Decides whether a consumer identified by consumer_group/consumer_project may ref() the named model, based on the contract's access level: public allows anyone, protected requires the same non-empty project, and private requires the same non-empty group. Also returns the contract's deprecation flag so a consumer can avoid ref-ing a deprecated model.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"consumer_group":"analytics","consumer_project":"revenue"},"schema":{"properties":{"consumer_group":{"description":"The consuming model's group; compared against the contract's owning group for private access.","type":"string"},"consumer_project":{"description":"The consuming model's project; compared against the contract's owning project for protected access.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"access":"protected","allowed":true,"contract":"orders.daily","deprecated":false},"schema":{"properties":{"access":{"description":"The contract's access level: private, protected, or public.","type":"string"},"allowed":{"description":"Whether the consumer may ref() this model under the access rules.","type":"boolean"},"contract":{"description":"The contract name evaluated.","type":"string"},"deprecated":{"description":"Whether the model is deprecated (a consumer may want to avoid ref-ing it even if allowed).","type":"boolean"}},"required":["contract","access","allowed","deprecated"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Decides whether a consumer identified by consumer_group/consumer_project may ref() the named model, based on the contract's access level: public allows anyone, protected requires the same non-empty project, and private requires the same non-empty group.","tags":["Model contracts"]}},"/api/v1/model-contracts/{name}/verify":{"post":{"description":"Verifies a produced output schema against the named contract's declared schema using strict dbt-contract semantics: a missing contracted column, an unexpected extra column, or a type mismatch is a violation. Returns 200 with ok=true when the produced schema matches exactly, or 422 with the list of violations on any breach.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"produced":{"columns":[{"name":"id","type":"int"},{"name":"name","type":"string"}]}},"schema":{"properties":{"produced":{"description":"The produced/observed schema to check, shaped {columns:[{name,type}]}. An empty/omitted schema is compared as having no columns.","type":"object"}},"required":["produced"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"contract":"orders.daily","ok":false,"version":"1.2.0","violations":["unexpected column \"extra\" (string) not in contract"]},"schema":{"properties":{"contract":{"description":"The contract name that was verified against.","type":"string"},"ok":{"description":"True iff there were zero violations (produced schema satisfies the contract exactly).","type":"boolean"},"version":{"description":"The contract's declared version.","type":"string"},"violations":{"description":"Human-readable violation strings (empty array when ok); e.g. missing contracted column, unexpected column not in contract, or type-violation messages.","items":{"type":"string"},"type":"array"}},"required":["contract","version","ok","violations"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Verifies a produced output schema against the named contract's declared schema using strict dbt-contract semantics: a missing contracted column, an unexpected extra column, or a type mismatch is a violation.","tags":["Model contracts"]}},"/api/v1/models":{"get":{"description":"Governed model picker (Copilot-style approved-models dropdown): returns only the LLM models the catalog both approves (present + admin-enabled) AND that are eligible for the requested capability and data classification. It is default-deny — an unapproved, disabled, incapable, or under-cleared model never appears. The response omits provider secrets/endpoints, exposing only the picker-relevant fields, and includes a suggested default (the first selectable model) when one exists. On a deployment running under INFERENCE_LOCAL_ONLY (the default) the catalog contains in-enclave models only, so the list and the default are in-enclave models and no external/cloud model is offered.","requestBody":{"content":{"application/json":{"schema":{"properties":{"capability":{"description":"Capability the model must advertise; one of text, vision, embed. Defaults to text when omitted or empty. Not validated against the enum — an unknown value simply matches no models (empty list).","type":"string"},"classification":{"description":"Data classification the model must be cleared for (MaxClassification \u003e= this); one of unclassified, cui, il4, il5. Empty/omitted is treated as unclassified. A non-empty value not in the enum returns 400.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"default":"local-vlm","models":[{"capabilities":["vision","text"],"display":"Local VLM (in-enclave)","id":"local-vlm","max_classification":"il5","provider":"local-vlm","residency":"local"}]},"schema":{"properties":{"default":{"description":"ID of the suggested default model (first selectable for the requested capability+classification). Omitted entirely when no model is selectable (e.g. capability=embed yields no models and no default key).","type":"string"},"models":{"description":"Approved + eligible models in catalog insertion order (stable admin-curated priority); always present (empty array, never null).","items":{"type":"object"},"type":"array"},"models[].capabilities":{"description":"What the model can do: subset of text, vision, embed.","items":{"type":"string"},"type":"array"},"models[].display":{"description":"Human-readable label to show in the picker.","type":"string"},"models[].id":{"description":"Stable governed catalog identity (e.g. local-vlm), not the provider's wire model name; the value callers pass back to select a model.","type":"string"},"models[].max_classification":{"description":"Highest data sensitivity this model may process: unclassified, cui, il4, or il5.","type":"string"},"models[].provider":{"description":"Router adapter key the model routes to (e.g. local-vlm). Under INFERENCE_LOCAL_ONLY only in-enclave adapters are registered, so only in-enclave keys can appear here.","type":"string"},"models[].residency":{"description":"Where the model runs: local (in-enclave, data never egresses) or cloud (external provider API, opt-in).","type":"string"}},"required":["models","models[].id","models[].provider","models[].display","models[].capabilities","models[].residency","models[].max_classification"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Governed model picker (Copilot-style approved-models dropdown): returns only the LLM models the catalog both approves (present + admin-enabled) AND that are eligible for the requested capability and data classification.","tags":["Models"]}},"/api/v1/models/catalog":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Models"],"x-stability":"experimental"}},"/api/v1/mtss/interventions":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/interventions/directory":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/interventions/seed":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/interventions/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/interventions/{id}/activate":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/interventions/{id}/assign-educator":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/interventions/{id}/assign-group":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/interventions/{id}/complete":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/interventions/{id}/deactivate":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/interventions/{id}/history":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/interventions/{id}/reactivate":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/library":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"},"put":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/plans":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/plans/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/plans/{id}/progress":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/mtss/plans/{id}/transition":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Mtss"],"x-stability":"experimental"}},"/api/v1/notebooks":{"get":{"description":"Lists every notebook in the caller's tenant as lightweight summaries (no cell bodies). It enumerates all objects under the tenant's notebooks/ prefix in the governed ObjectStore, decodes each, and returns them sorted by updated_at descending (newest first); records that fail to decode or have an empty id are silently skipped.","responses":{"200":{"content":{"application/json":{"example":{"notebooks":[{"cell_count":6,"created_by":"maya.analyst","id":"000c72647ce5f25d","name":"Cell chaining: passing variables between cells","updated_at":"2026-07-04T17:17:30.370658721Z"},{"cell_count":7,"created_by":"raj.dataeng","id":"cf074afb105d3000","name":"Writing Tables \u0026 Files from a Notebook","updated_at":"2026-07-04T17:07:50.863338142Z"},{"cell_count":4,"created_by":"tom.scientist","id":"996380124b12ffee","name":"Population Modeling","updated_at":"2026-07-04T16:15:37.454986454Z"},{"cell_count":4,"created_by":"sofia.admin","id":"b57f280d080c7e2c","name":"Orchestration Runbook","updated_at":"2026-07-04T16:15:36.145414911Z"},{"cell_count":4,"created_by":"raj.dataeng","id":"a4740fdb2565621a","name":"Weather \u0026 Sonar Field Notes","updated_at":"2026-07-04T16:15:32.734809688Z"},{"cell_count":5,"created_by":"maya.analyst","id":"bd533e3796a76039","name":"Gapminder Exploration","updated_at":"2026-07-04T16:15:30.139508712Z"}]},"schema":{"properties":{"notebooks":{"description":"Array of notebook summaries, newest-updated first.","items":{"type":"object"},"type":"array"},"notebooks[].cell_count":{"description":"Number of cells in the notebook.","type":"integer"},"notebooks[].created_by":{"description":"Subject that created the notebook.","type":"string"},"notebooks[].id":{"description":"Opaque 16-hex notebook identifier.","type":"string"},"notebooks[].name":{"description":"Human-readable notebook name.","type":"string"},"notebooks[].updated_at":{"description":"RFC3339Nano timestamp of last update; also the sort key.","type":"string"}},"required":["notebooks","notebooks[].id","notebooks[].name","notebooks[].cell_count","notebooks[].created_by","notebooks[].updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists every notebook in the caller's tenant as lightweight summaries (no cell bodies).","tags":["Notebooks"]},"post":{"description":"Creates a new notebook seeded with a single SQL cell containing \"SELECT 1\". Only the name is caller-supplied (defaulting to \"Untitled notebook\" when blank); the server assigns the id, created_by (from the request subject), and created_at/updated_at timestamps, then persists it and echoes the full created document.","requestBody":{"content":{"application/json":{"example":{"name":"Population Modeling"},"schema":{"properties":{"name":{"description":"Notebook name; trimmed, defaults to \"Untitled notebook\" when empty/blank.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"cells":[{"id":"c1d2e3f405162738","kind":"sql","source":"SELECT 1"}],"created_at":"2026-07-15T23:25:00.123456789Z","created_by":"maya.analyst","id":"a1b2c3d4e5f60718","name":"Population Modeling","updated_at":"2026-07-15T23:25:00.123456789Z"},"schema":{"properties":{"cells":{"description":"Seed cells: one SQL cell with source \"SELECT 1\".","items":{"type":"object"},"type":"array"},"created_at":{"description":"RFC3339Nano creation timestamp.","type":"string"},"created_by":{"description":"Subject that created the notebook.","type":"string"},"id":{"description":"Server-generated notebook id.","type":"string"},"name":{"description":"Notebook name (or the default).","type":"string"},"updated_at":{"description":"RFC3339Nano timestamp (equals created_at on create).","type":"string"}},"required":["id","name","cells","created_by","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates a new notebook seeded with a single SQL cell containing \"SELECT 1\".","tags":["Notebooks"]}},"/api/v1/notebooks/import":{"post":{"description":"Imports a Jupyter .ipynb (nbformat v4) document as a brand-new notebook. It accepts either a multipart file part or the raw request body as the .ipynb bytes (capped at 32 MiB), maps nbformat cells to platform cells (markdown/raw-\u003emarkdown, code-\u003epython, and a code cell leading with %%sql or %sql -\u003esql with the magic line stripped), assigns a fresh id/created_by/timestamps, and returns the created notebook.","requestBody":{"content":{"application/json":{"example":{"cells":[{"cell_type":"markdown","source":"# Imported"},{"cell_type":"code","source":"%%sql\nSELECT 1"},{"cell_type":"code","source":"print('hi')"}],"metadata":{},"nbformat":4,"nbformat_minor":5},"schema":{"properties":{"(body)":{"description":"The .ipynb content: either a multipart file part (real file picker) or the raw request body as nbformat JSON.","type":"string"},"name":{"description":"Query param ?name=: notebook name for the raw-body path (trailing .ipynb stripped); ignored when a multipart filename is present.","type":"string"}},"required":["(body)"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"cells":[{"id":"c1a2b3c4d5e6f708","kind":"markdown","source":"# Imported"},{"id":"c2b3c4d5e6f70819","kind":"sql","source":"SELECT 1"},{"id":"c3c4d5e6f708192a","kind":"python","source":"print('hi')"}],"created_at":"2026-07-15T23:25:00.123456789Z","created_by":"maya.analyst","id":"b7c8d9e0f1a2b3c4","name":"Imported","updated_at":"2026-07-15T23:25:00.123456789Z"},"schema":{"properties":{"cells":{"description":"Cells mapped from the .ipynb; a document with no usable cells yields one markdown placeholder cell.","items":{"type":"object"},"type":"array"},"created_at":{"description":"RFC3339Nano creation timestamp.","type":"string"},"created_by":{"description":"Subject that imported the notebook.","type":"string"},"id":{"description":"Server-generated notebook id.","type":"string"},"name":{"description":"Name from ?name=/filename, else the vforce_lakehouse metadata name, else \"Imported notebook\".","type":"string"},"updated_at":{"description":"RFC3339Nano timestamp (equals created_at on import).","type":"string"}},"required":["id","name","cells","created_by","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Imports a Jupyter .ipynb (nbformat v4) document as a brand-new notebook.","tags":["Notebooks"]}},"/api/v1/notebooks/{id}":{"delete":{"description":"Deletes the notebook object notebooks/{id}.json from the tenant's ObjectStore and returns 204 No Content with an empty body.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the notebook id to delete.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes the notebook object notebooks/{id}.json from the tenant's ObjectStore and returns 204 No Content with an empty body.","tags":["Notebooks"]},"get":{"description":"Fetches one full notebook by id, including every cell's source and per-cell execution metadata. It reads notebooks/{id}.json from the tenant's ObjectStore and returns the decoded document as-is.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the notebook id.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"cells":[{"id":"c-a4b7a277","kind":"markdown","source":"# Population Modeling\n\nExploring population + GDP correlations for a research note."},{"id":"c-8d199d19","kind":"sql","source":"SELECT p.country, p.population, g.gdp_usd FROM demo.reference.population p JOIN demo.reference.gdp g ON p.country_code = g.country_code ORDER BY g.gdp_usd DESC LIMIT 15"},{"id":"c-8b1b77a0","kind":"sql","source":"SELECT species, avg(body_mass_g) AS avg_mass FROM demo.research.penguins GROUP BY species"},{"id":"c-5af9b497","kind":"python","source":"# rough correlation sketch (placeholder, run in Pyodide)\nimport statistics\nvals = [3706, 3709, 3688, 3733, 5076]\nstatistics.mean(vals)"}],"created_at":"2026-07-04T16:15:37.41426076Z","created_by":"tom.scientist","id":"996380124b12ffee","name":"Population Modeling","updated_at":"2026-07-04T16:15:37.454986454Z"},"schema":{"properties":{"cells":{"description":"Ordered cells (id, kind, source, and optional last_run_status/last_run_at).","items":{"type":"object"},"type":"array"},"cells[].id":{"description":"Cell identifier.","type":"string"},"cells[].kind":{"description":"Cell kind: \"sql\" | \"markdown\" | \"python\".","type":"string"},"cells[].last_run_at":{"description":"RFC3339 timestamp of last run; omitted if never run.","type":"string"},"cells[].last_run_status":{"description":"Last execution status \"ok\"|\"error\"|\"skipped\"; omitted if never run.","type":"string"},"cells[].source":{"description":"Cell source text (SQL, markdown, or Python).","type":"string"},"created_at":{"description":"RFC3339Nano creation timestamp.","type":"string"},"created_by":{"description":"Subject that created the notebook.","type":"string"},"id":{"description":"Notebook identifier.","type":"string"},"name":{"description":"Notebook name.","type":"string"},"updated_at":{"description":"RFC3339Nano last-update timestamp.","type":"string"}},"required":["id","name","cells","cells[].id","cells[].kind","cells[].source","created_by","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Fetches one full notebook by id, including every cell's source and per-cell execution metadata.","tags":["Notebooks"]},"put":{"description":"Replaces a notebook's name and full cell list, preserving the original created_by/created_at from the stored record (loaded first). It bumps updated_at to now; if no prior record exists it treats the PUT as an upsert, stamping created_at/created_by from the current request. Blank name falls back to \"Untitled notebook\".","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"cells":[{"id":"c-a4b7a277","kind":"markdown","source":"# Population Modeling"},{"id":"c-8d199d19","kind":"sql","last_run_at":"2026-07-15T23:00:00Z","last_run_status":"ok","source":"SELECT 1"}],"name":"Population Modeling"},"schema":{"properties":{"cells":{"description":"Full replacement cell array; each cell is {id, kind, source, last_run_status?, last_run_at?}.","items":{"type":"object"},"type":"array"},"cells[].id":{"description":"Cell id (caller-managed).","type":"string"},"cells[].kind":{"description":"\"sql\" | \"markdown\" | \"python\".","type":"string"},"cells[].last_run_at":{"description":"Persisted last-run timestamp.","type":"string"},"cells[].last_run_status":{"description":"Persisted execution status \"ok\"|\"error\"|\"skipped\".","type":"string"},"cells[].source":{"description":"Cell source text.","type":"string"},"id":{"description":"Path parameter: the notebook id to write.","type":"string"},"name":{"description":"New notebook name; trimmed, defaults to \"Untitled notebook\" when blank.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"cells":[{"id":"c-a4b7a277","kind":"markdown","source":"# Population Modeling"},{"id":"c-8d199d19","kind":"sql","last_run_at":"2026-07-15T23:00:00Z","last_run_status":"ok","source":"SELECT 1"}],"created_at":"2026-07-04T16:15:37.41426076Z","created_by":"tom.scientist","id":"996380124b12ffee","name":"Population Modeling","updated_at":"2026-07-15T23:25:00.123456789Z"},"schema":{"properties":{"cells":{"description":"The cell array as written.","items":{"type":"object"},"type":"array"},"created_at":{"description":"Preserved creation timestamp, or now on first write.","type":"string"},"created_by":{"description":"Preserved from the existing record, or the request subject on first write.","type":"string"},"id":{"description":"Notebook id (from the path).","type":"string"},"name":{"description":"Updated name (or the default).","type":"string"},"updated_at":{"description":"RFC3339Nano timestamp set to now.","type":"string"}},"required":["id","name","cells","created_by","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Replaces a notebook's name and full cell list, preserving the original created_by/created_at from the stored record (loaded first).","tags":["Notebooks"]}},"/api/v1/notebooks/{id}/export":{"get":{"description":"Exports a stored notebook as a downloadable Jupyter .ipynb (nbformat v4) file. Markdown cells become markdown cells, SQL cells become code cells prefixed with the %%sql cell magic, and Python (and unrecognised) cells become plain code cells; a vforce_lakehouse metadata block carries the notebook_id/name so a re-import keeps identity.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the notebook id to export.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Population Modeling\n","\n","Exploring population + GDP correlations for a research note."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["%%sql\n","SELECT p.country, p.population, g.gdp_usd FROM demo.reference.population p JOIN demo.reference.gdp g ON p.country_code = g.country_code ORDER BY g.gdp_usd DESC LIMIT 15"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["%%sql\n","SELECT species, avg(body_mass_g) AS avg_mass FROM demo.research.penguins GROUP BY species"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["# rough correlation sketch (placeholder, run in Pyodide)\n","import statistics\n","vals = [3706, 3709, 3688, 3733, 5076]\n","statistics.mean(vals)"]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python"},"vforce_lakehouse":{"name":"Population Modeling","notebook_id":"996380124b12ffee"}},"nbformat":4,"nbformat_minor":5},"schema":{"properties":{"(body)":{"description":"An indented nbformat-v4 JSON document (application/x-ipynb+json) with cells, metadata (kernelspec, language_info, vforce_lakehouse{notebook_id,name}), nbformat=4, nbformat_minor=5.","type":"object"}},"required":["(body)"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Exports a stored notebook as a downloadable Jupyter .ipynb (nbformat v4) file.","tags":["Notebooks"]}},"/api/v1/notifications":{"get":{"description":"Returns the authenticated caller's own asset-change notifications, sorted newest-first by creation time. It lists every record under the tenant-scoped `notifications/` prefix in the object store, decodes each, and keeps only those whose `subscriber` equals the caller's subject; notifications are produced when someone publishes a schema/deprecation/freshness/quality/ownership event on a catalog asset the caller is subscribed to.","responses":{"200":{"content":{"application/json":{"example":{"notifications":[]},"schema":{"properties":{"notifications":{"description":"Array of the caller's notification objects, newest first (empty array when none).","items":{"type":"object"},"type":"array"},"notifications[].created_at":{"description":"RFC3339 UTC timestamp when the notification was created.","type":"string"},"notifications[].event":{"description":"Event type: one of schema, deprecation, freshness, quality, ownership.","type":"string"},"notifications[].id":{"description":"Unique notification id (16-hex-char random).","type":"string"},"notifications[].message":{"description":"Operator-supplied message describing the change (capped at 2000 chars at publish time).","type":"string"},"notifications[].subscriber":{"description":"Subject the notification was delivered to (always the caller in this response).","type":"string"},"notifications[].table":{"description":"Fully-qualified catalog.schema.table the change event concerns.","type":"string"}},"required":["notifications","notifications[].id","notifications[].subscriber","notifications[].table","notifications[].event","notifications[].message","notifications[].created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the authenticated caller's own asset-change notifications, sorted newest-first by creation time.","tags":["Notifications"]}},"/api/v1/object-geometry":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Object geometry"],"x-stability":"experimental"}},"/api/v1/object-image":{"get":{"description":"Serves the scanned page image for the document viewer as a binary JPEG. This is the proxy-safe variant of GET /api/v1/objects/{key}/image that takes the object key as a ?key= query parameter so multi-segment keys (e.g. ingest/pdfdocs/foo.pdf#p8) survive the Next.js catch-all proxy, which decodes %2F and breaks the single-segment {key} path route. Resolution order: (1) stream the stored compact preview preview/\u003ckey\u003e.jpg as image/jpeg (fast path, O(copy-buffer) memory); (2) if no preview but a retained/legacy master exists, reduced-decode the master to JPEG (bounded: oversized masters are refused and decodes are serialized through a 2-slot semaphore), serve it, and lazily persist the preview for the next hit; (3) otherwise 404.","requestBody":{"content":{"application/json":{"schema":{"properties":{"key":{"description":"Object/page key (the gold row's key). Effectively mandatory: a 400 'missing key' is returned when absent. The same handler also accepts the key as the {key} path segment on the sibling /objects/{key}/image route; on this route the ?key= form is used because it survives the Next.js proxy for multi-segment keys.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"(body)":{"description":"Raw JPEG bytes (Content-Type: image/jpeg). NOT JSON — the handler writes the image via streamImageJPEG/writeImageJPEG, not writeJSON. Cache-Control: private, max-age=86400, immutable. Preview long-side budget 1600px, JPEG quality 82 for lazily-built previews.","type":"string"}},"required":["(body)"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Serves the scanned page image for the document viewer as a binary JPEG.","tags":["Object image"]}},"/api/v1/objects/{key}/boxes":{"get":{"description":"Serves the per-word bounding boxes recorded for a page so the viewer can highlight and zoom to a search or selection directly on the scanned image. Boxes are normalized to the [0,1] coordinate space; a page with no recorded boxes returns 200 with an empty list rather than an error.","parameters":[{"in":"path","name":"key","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"key":{"description":"Page/object key, taken from the {key} path segment (no ?key= fallback for this route).","type":"string"}},"required":["key"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"boxes":[]},"schema":{"properties":{"boxes":{"description":"Array of word boxes; empty array when none are recorded. Each element has fields t (string, the recognized word text), x0, y0, x1, y1 (float64, normalized [0,1] corner coordinates).","items":{"type":"object"},"type":"array"}},"required":["boxes"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Serves the per-word bounding boxes recorded for a page so the viewer can highlight and zoom to a search or selection directly on the scanned image.","tags":["Objects"]}},"/api/v1/objects/{key}/geometry":{"get":{"parameters":[{"in":"path","name":"key","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Objects"],"x-stability":"experimental"}},"/api/v1/objects/{key}/image":{"get":{"description":"Serves the scanned page image for the document viewer, where {key} is the gold row's page key. It resolves in order: (1) stream the compact preview JPEG (preview/\u003ckey\u003e.jpg) produced once at ingest, (2) if no preview exists but a master object does, reduced-decode the master to JPEG, serve it, and lazily persist the preview for the next request, or (3) return 404 when neither exists. Oversized masters (over the on-demand decode limit) are refused with 422 rather than decoded on the fly.","parameters":[{"in":"path","name":"key","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"key":{"description":"Object/page key; normally the {key} path segment, but may instead be supplied as ?key= (proxy-safe form that survives the Next.js catch-all decoding %2F in multi-segment keys). One of the two is required.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"(binary)":{"description":"Raw image/jpeg body (not JSON). Sent with Content-Type: image/jpeg and Cache-Control: private, max-age=86400, immutable. Preview hits are streamed straight from the object store without buffering the whole image in the pod heap.","type":"string"}},"required":["(binary)"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Serves the scanned page image for the document viewer, where {key} is the gold row's page key.","tags":["Objects"]}},"/api/v1/observability/detect":{"post":{"description":"Runs the stateless data-observability incident detector over a single table's operational metadata: it compares a caller-supplied current snapshot against a baseline and returns a report of freshness (stale load vs SLA), volume (row-count deviation from the trailing mean, or a drop to zero), and schema-drift (columns added/removed) incidents, each with a human-readable root-cause description. The detection is a pure function with the wall clock (now = time.Now().UTC()) injected at the handler boundary; the service stores nothing, so the same snapshot+baseline is deterministic.","requestBody":{"content":{"application/json":{"example":{"baseline":{"columns":["id","name","email"],"max_freshness_seconds":3600,"row_counts":[1000,1050,980],"volume_max_deviation":0.5},"snapshot":{"columns":["id","name"],"loaded_at":"2026-07-10T00:00:00Z","row_count":0}},"schema":{"properties":{"baseline":{"description":"Expected state to compare against.","type":"object"},"baseline.columns":{"description":"Last-known schema. Empty skips the schema-drift check.","items":{"type":"string"},"type":"array"},"baseline.max_freshness_seconds":{"description":"Staleness SLA in seconds. \u003c=0 skips the freshness check.","type":"integer"},"baseline.row_counts":{"description":"Trailing history of prior-run row counts; its mean is the volume baseline. Empty skips the volume check.","items":{"type":"integer"},"type":"array"},"baseline.volume_max_deviation":{"description":"Fractional tolerance around the mean before a volume warning fires (e.g. 0.5 = plus/minus 50%). \u003c=0 defaults to 0.5.","type":"number"},"snapshot":{"description":"Current observed state of the table (row_count, columns, loaded_at).","type":"object"},"snapshot.columns":{"description":"Current column names, compared against baseline.columns for schema drift.","items":{"type":"string"},"type":"array"},"snapshot.loaded_at":{"description":"RFC3339 timestamp of last load; empty string skips the freshness check (unparseable also silently skips it).","type":"string"},"snapshot.row_count":{"description":"Current row count; 0 triggers an error-severity volume incident when a baseline history exists.","type":"integer"}},"required":["snapshot","baseline"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"healthy":false,"incidents":[{"description":"data is stale: loaded 120h0m0s ago, SLA is 3600s","severity":"error","signal":"freshness"},{"description":"schema drift: added [], removed [email]","severity":"warning","signal":"schema"},{"description":"row count dropped to zero (expected ~1010)","severity":"error","signal":"volume"}]},"schema":{"properties":{"healthy":{"description":"True iff no incidents were detected.","type":"boolean"},"incidents":{"description":"Detected anomalies, ordered deterministically by signal name (alphabetical: freshness, schema, volume). Always present; empty array when healthy.","items":{"type":"object"},"type":"array"},"incidents[].description":{"description":"Human-readable root-cause detail for the incident.","type":"string"},"incidents[].severity":{"description":"\"error\" (freshness breach, or row count dropped to zero) or \"warning\" (volume deviation, schema drift).","type":"string"},"incidents[].signal":{"description":"Which check fired: \"freshness\", \"volume\", or \"schema\".","type":"string"}},"required":["incidents","incidents[].signal","incidents[].severity","incidents[].description","healthy"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs the stateless data-observability incident detector over a single table's operational metadata: it compares a caller-supplied current snapshot against a baseline and returns a report of freshness (stale load vs SLA), volume (row-count deviation from the trailing mean, or a drop to zero), and schema-drift (columns added/removed) incidents, each with a human-readable root-cause description.","tags":["Observability"]}},"/api/v1/orchestration/runs/stream":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Orchestration"],"x-stability":"experimental"}},"/api/v1/partners/access-review":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Partners"],"x-stability":"experimental"}},"/api/v1/partners/caseloads/reconcile":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Partners"],"x-stability":"experimental"}},"/api/v1/partners/deprovision":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Partners"],"x-stability":"experimental"}},"/api/v1/partners/deprovisions":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Partners"],"x-stability":"experimental"}},"/api/v1/partners/invitations":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Partners"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Partners"],"x-stability":"experimental"}},"/api/v1/partners/invitations/accept":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Partners"],"x-stability":"experimental"}},"/api/v1/partners/invitations/{id}/revoke":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Partners"],"x-stability":"experimental"}},"/api/v1/projections/rebuild":{"post":{"description":"Rebuilds the tenant's event-projection views: for each discovered CloudEvents table it reads the distinct event `type` values, samples up to 200 payloads per type to infer a schema, and issues CREATE OR REPLACE VIEW statements on Spark/Livy that expose a typed PARENT view (payload scalars become columns) plus one CHILD view per array field (posexplode into event_id/ordinal/element columns), registering each view in the catalog's `projected` schema so the opaque event blobs become browsable typed tables. The run is idempotent and tolerant of partial failure — a Spark or registry error on one event type is recorded in that type's result and the rebuild continues with the rest.","requestBody":{"content":{"application/json":{"example":{"catalog":"isd","schema":"events"},"schema":{"properties":{"catalog":{"description":"Catalog to scope discovery to; when combined with schema it is used verbatim, otherwise it filters/hints the events-namespace discovery. Empty discovers every events namespace for the tenant.","type":"string"},"schema":{"description":"Schema to project; only used verbatim when catalog is also set. Otherwise discovery targets the conventional `events` schema across namespaces.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"compute_error":false,"results":[{"samples":200,"status":"ok","table":"isd.events.courses","type":"course.created.v1","views":[{"columns":7,"kind":"parent","view":"isd.projected.course_created_v1"},{"array":"modules","columns":4,"kind":"child","view":"isd.projected.course_created_v1__modules"}]},{"error":"view creation failed (parent): org.apache.spark.sql.AnalysisException: ...","samples":12,"status":"error","table":"isd.events.enrollments","type":"enrollment.updated.v1"}],"tenant":"acme","views_built":3},"schema":{"properties":{"compute_error":{"description":"True if at least one source events table could not be read because compute (Livy/Spark) was unavailable — distinguishes a compute outage from a genuinely empty result.","type":"boolean"},"results":{"description":"Per-(table,type) outcomes. Each entry has table (source FQN), type (CloudEvent type), status (\"ok\"|\"error\"), error (reason when status=error), samples (payloads sampled), and views (array of {view, kind (\"parent\"|\"child\"), columns, array}). May be an empty array when nothing was discovered.","items":{"type":"object"},"type":"array"},"tenant":{"description":"The tenant ID the rebuild ran against (resolved from the request, via tenantOf).","type":"string"},"views_built":{"description":"Total number of views successfully created and registered across all tables and event types in this run.","type":"integer"}},"required":["tenant","views_built","compute_error","results"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Rebuilds the tenant's event-projection views: for each discovered CloudEvents table it reads the distinct event `type` values, samples up to 200 payloads per type to infer a schema, and issues CREATE OR REPLACE VIEW statements on Spark/Livy that expose a typed PARENT view (payload scalars become columns) plus one CHILD view per array field (posexplode into event_id/ordinal/element columns), registering each view in the catalog's `projected` schema so the opaque event blobs become browsable typed tables.","tags":["Projections"],"x-stability":"experimental"}},"/api/v1/quality/schema/{fqn}":{"get":{"parameters":[{"in":"path","name":"fqn","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Quality"],"x-stability":"experimental"},"put":{"parameters":[{"in":"path","name":"fqn","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Quality"],"x-stability":"experimental"}},"/api/v1/queries":{"get":{"description":"Returns the calling tenant's saved SQL queries — the SQL editor's \"My queries\" surface — by listing every object under the saved-queries/ prefix in the tenant-scoped object store, unmarshalling each JSON record, and returning them sorted newest-first by CreatedAt. Records that fail a transient read or fail to unmarshal (or have an empty id) are silently skipped rather than blanking the whole list.","responses":{"200":{"content":{"application/json":{"example":{"queries":[{"created_at":"2026-07-05T13:52:32Z","created_by":"anonymous","id":"c0974af903ae60bc","name":"average gdp per cap","sql":"SELECT continent, avg(avg_gdp_percap) AS gdp_percap FROM demo.gold.gapminder_by_continent GROUP BY continent ORDER BY gdp_percap DESC"},{"created_at":"2026-07-04T16:14:55Z","created_by":"tom.scientist","id":"409626acf84aeacc","name":"Course Authoring Activity","sql":"SELECT course_id, status, duration_ms FROM isd.projected.course_authored ORDER BY time DESC LIMIT 20","tags":["isd","learning"]},{"created_at":"2026-07-04T16:14:54Z","created_by":"tom.scientist","id":"88ca82cf97cba033","name":"Penguins by Species and Island","sql":"SELECT species, island, count(*) AS n, avg(body_mass_g) AS avg_mass FROM demo.research.penguins GROUP BY species, island ORDER BY species","tags":["research"]}]},"schema":{"properties":{"queries":{"description":"Array of saved-query objects, newest first (sorted descending by created_at).","items":{"type":"object"},"type":"array"},"queries[].created_at":{"description":"RFC3339 UTC creation timestamp (used as the lexicographic sort key).","type":"string"},"queries[].created_by":{"description":"Subject (user) that created the query, from the request identity; \"anonymous\" on the public demo.","type":"string"},"queries[].id":{"description":"Short random hex id (16 hex chars / 8 bytes) assigned at creation.","type":"string"},"queries[].name":{"description":"Human-readable query name.","type":"string"},"queries[].sql":{"description":"The stored SQL text.","type":"string"},"queries[].tags":{"description":"Optional tag labels; omitted from JSON when empty (omitempty).","items":{"type":"string"},"type":"array"}},"required":["queries","queries[].id","queries[].name","queries[].sql","queries[].created_by","queries[].created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the calling tenant's saved SQL queries — the SQL editor's \"My queries\" surface — by listing every object under the saved-queries/ prefix in the tenant-scoped object store, unmarshalling each JSON record, and returning them sorted newest-first by CreatedAt.","tags":["Queries"]},"post":{"description":"Creates and stores a new saved SQL query for the calling tenant: it trims and validates that name and sql are non-empty, cleans the tag list, assigns a random hex id and an RFC3339 UTC created_at, records created_by from the request subject, and persists the record as a JSON object (DocKind \"saved-query\") under the saved-queries/ prefix in the tenant object store. Returns 201 with the stored object.","requestBody":{"content":{"application/json":{"example":{"name":"Top 10 Most Populous Countries","sql":"SELECT country, population, year FROM demo.reference.population ORDER BY population DESC LIMIT 10","tags":["reference","population"]},"schema":{"properties":{"name":{"description":"Query name; trimmed and must be non-empty (400 otherwise).","type":"string"},"sql":{"description":"SQL text; trimmed and must be non-empty (400 otherwise).","type":"string"},"tags":{"description":"Optional tags; each is trimmed, blanks dropped, and the list capped at 8 entries by cleanTags.","items":{"type":"string"},"type":"array"}},"required":["name","sql"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-15T12:00:00Z","created_by":"maya.analyst","id":"a1b2c3d4e5f60718","name":"Top 10 Most Populous Countries","sql":"SELECT country, population, year FROM demo.reference.population ORDER BY population DESC LIMIT 10","tags":["reference","population"]},"schema":{"properties":{"created_at":{"description":"RFC3339 UTC creation timestamp.","type":"string"},"created_by":{"description":"Subject from the request identity (subjectOf).","type":"string"},"id":{"description":"Server-assigned short random hex id (16 hex chars).","type":"string"},"name":{"description":"The trimmed query name.","type":"string"},"sql":{"description":"The trimmed SQL text.","type":"string"},"tags":{"description":"Cleaned tag list (trimmed, no blanks, max 8); omitted when empty.","items":{"type":"string"},"type":"array"}},"required":["id","name","sql","created_by","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates and stores a new saved SQL query for the calling tenant: it trims and validates that name and sql are non-empty, cleans the tag list, assigns a random hex id and an RFC3339 UTC created_at, records created_by from the request subject, and persists the record as a JSON object (DocKind \"saved-query\") under the saved-queries/ prefix in the tenant object store.","tags":["Queries"]}},"/api/v1/queries/{id}":{"delete":{"description":"Deletes one saved query by its id from the calling tenant's object store (removes the saved-queries/{id}.json object). The operation is idempotent — deleting a non-existent id still succeeds — and returns 204 No Content with no body.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the saved-query id to delete; trimmed and must be non-empty (400 otherwise).","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes one saved query by its id from the calling tenant's object store (removes the saved-queries/{id}.json object).","tags":["Queries"]}},"/api/v1/query-history":{"get":{"description":"Returns the calling tenant's SQL query-history log — every executed query (success or failure) recorded under the tenant's query-history/ object-store prefix — sorted newest first by execution timestamp. Each entry captures the SQL text, outcome status, row count, wall-clock duration, who ran it, and when; the response also reports the total number of history entries in the estate so a client can tell how much history exists beyond what was returned.","requestBody":{"content":{"application/json":{"schema":{"properties":{"limit":{"description":"Query param: cap the number of entries returned. Only honored if a valid integer \u003e0 and strictly less than the hard cap of 200; otherwise the 200-entry cap applies. Values \u003e=200, \u003c=0, or non-numeric are ignored.","type":"integer"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"history":[{"duration_ms":2016,"id":"1db675c985a0721f","ran_at":"2026-07-15T23:32:53.909828292Z","ran_by":"anonymous","row_count":2,"sql":"SELECT event_id, status FROM orchestration.projected.run_started LIMIT 2","status":"ok"},{"duration_ms":2014,"id":"6e622557701666c1","ran_at":"2026-07-15T23:32:51.672031448Z","ran_by":"anonymous","row_count":2,"sql":"SELECT event_id, status, flow_id FROM orchestration.projected.run_started LIMIT 2","status":"ok"}],"total":460},"schema":{"properties":{"history":{"description":"Array of query-history entries, newest first (bounded by limit / the 200 hard cap).","items":{"type":"object"},"type":"array"},"history[].duration_ms":{"description":"Execution wall-clock time in milliseconds.","type":"integer"},"history[].error":{"description":"Error message when status is \"error\"; omitted (json omitempty) on success.","type":"string"},"history[].id":{"description":"Unique identifier of the history entry (16-hex-char generated ID).","type":"string"},"history[].ran_at":{"description":"Execution timestamp, RFC3339Nano UTC (lexicographically sortable; used for newest-first ordering).","type":"string"},"history[].ran_by":{"description":"Identity that ran the query (e.g. \"anonymous\" on the public demo).","type":"string"},"history[].row_count":{"description":"Number of rows the query returned (0 on error).","type":"integer"},"history[].sql":{"description":"The SQL text that was executed.","type":"string"},"history[].status":{"description":"Outcome of the execution: \"ok\" or \"error\".","type":"string"},"total":{"description":"Total number of history entries for the tenant before the limit/cap was applied.","type":"integer"}},"required":["history","history[].id","history[].sql","history[].status","history[].row_count","history[].duration_ms","history[].ran_by","history[].ran_at","total"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the calling tenant's SQL query-history log — every executed query (success or failure) recorded under the tenant's query-history/ object-store prefix — sorted newest first by execution timestamp.","tags":["Query history"]}},"/api/v1/referrals":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Referrals"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Referrals"],"x-stability":"experimental"}},"/api/v1/referrals/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Referrals"],"x-stability":"experimental"}},"/api/v1/referrals/{id}/service-units":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Referrals"],"x-stability":"experimental"}},"/api/v1/referrals/{id}/transition":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Referrals"],"x-stability":"experimental"}},"/api/v1/registry/models":{"get":{"description":"Lists every RegisteredModel in the caller's tenant registry namespace. Returns the models array (never null; an empty registry yields []) with no versions attached.","responses":{"200":{"content":{"application/json":{"example":{"models":[]},"schema":{"properties":{"models":{"description":"Array of RegisteredModel objects; empty array when the tenant has none.","items":{"type":"object"},"type":"array"},"models[].created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"models[].description":{"description":"Free-text description (omitempty).","type":"string"},"models[].id":{"description":"Server-assigned model id.","type":"string"},"models[].name":{"description":"Model name, unique within (tenant, name).","type":"string"},"models[].owner":{"description":"Owner identity (omitempty).","type":"string"},"models[].tags":{"description":"Free-form tags (omitempty).","items":{"type":"string"},"type":"array"},"models[].task":{"description":"Model task: classification|extraction|scoring|embedding|generation (omitempty).","type":"string"},"models[].tenant_id":{"description":"Owning tenant id.","type":"string"},"models[].updated_at":{"description":"RFC3339 last-update timestamp.","type":"string"}},"required":["models","models[].id","models[].tenant_id","models[].name","models[].created_at","models[].updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists every RegisteredModel in the caller's tenant registry namespace.","tags":["Registry"]},"post":{"description":"Registers a new named model in the caller's tenant registry. The handler decodes a JSON body and calls RegisterModel; the server assigns id/timestamps. Returns 201 with the created RegisteredModel, or 409 on conflict (e.g. duplicate name).","requestBody":{"content":{"application/json":{"example":{"description":"Classifies incoming invoices","name":"invoice-classifier","owner":"ml-team","tags":["idp","finance"],"task":"classification"},"schema":{"properties":{"description":{"description":"Free-text description.","type":"string"},"name":{"description":"Model name; uniqueness within (tenant, name) is enforced by the adapter (conflict =\u003e 409).","type":"string"},"owner":{"description":"Owner identity string.","type":"string"},"tags":{"description":"Free-form tags.","items":{"type":"string"},"type":"array"},"task":{"description":"Model task: classification|extraction|scoring|embedding|generation.","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-15T12:00:00Z","description":"Classifies incoming invoices","id":"mdl_01H...","name":"invoice-classifier","owner":"ml-team","tags":["idp","finance"],"task":"classification","tenant_id":"acme","updated_at":"2026-07-15T12:00:00Z"},"schema":{"properties":{"created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"description":{"description":"Description (omitempty).","type":"string"},"id":{"description":"Server-assigned model id.","type":"string"},"name":{"description":"Model name.","type":"string"},"owner":{"description":"Owner (omitempty).","type":"string"},"tags":{"description":"Tags (omitempty).","items":{"type":"string"},"type":"array"},"task":{"description":"Model task (omitempty).","type":"string"},"tenant_id":{"description":"Owning tenant id (from tenantOf(r)).","type":"string"},"updated_at":{"description":"RFC3339 update timestamp.","type":"string"}},"required":["id","tenant_id","name","created_at","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Registers a new named model in the caller's tenant registry.","tags":["Registry"]}},"/api/v1/registry/models/{name}":{"get":{"description":"Fetches one RegisteredModel by name together with all of its versions. Returns 404 if the model does not exist in the tenant; otherwise the model object plus its versions array (never null).","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"description":"Path parameter: the registered model name.","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"detail":"model \"foo\" not found","status":404,"title":"not found","type":"about:blank"},"schema":{"properties":{"model":{"description":"The RegisteredModel (id, tenant_id, name, description, task, owner, tags, created_at, updated_at).","type":"object"},"versions":{"description":"Array of ModelVersion objects for this model; empty array if none.","items":{"type":"object"},"type":"array"},"versions[].artifact_uri":{"description":"URI of the stored model artifact (omitempty).","type":"string"},"versions[].classification":{"description":"Highest data sensitivity cleared: unclassified|cui|il4|il5 (omitempty).","type":"string"},"versions[].created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"versions[].created_by":{"description":"Creator identity (omitempty).","type":"string"},"versions[].description":{"description":"Version description (omitempty).","type":"string"},"versions[].framework":{"description":"Training framework (omitempty).","type":"string"},"versions[].metrics":{"description":"Evaluation metrics (omitempty).","type":"string"},"versions[].model_name":{"description":"Parent model name.","type":"string"},"versions[].source_run_id":{"description":"Originating training run id (omitempty).","type":"string"},"versions[].stage":{"description":"Lifecycle stage: None|Staging|Production|Archived.","type":"string"},"versions[].tenant_id":{"description":"Owning tenant id.","type":"string"},"versions[].training_dataset_fqn":{"description":"FQN of the training dataset (omitempty).","type":"string"},"versions[].version":{"description":"Immutable version number (\u003e=1).","type":"integer"}},"required":["model","versions","versions[].tenant_id","versions[].model_name","versions[].version","versions[].stage","versions[].created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Fetches one RegisteredModel by name together with all of its versions.","tags":["Registry"]}},"/api/v1/registry/models/{name}/production":{"get":{"description":"Returns the single ModelVersion currently in the Production stage for the named model (at most one Production version per tenant+model). Responds 404 when the model has no Production version.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"description":"Path parameter: the registered model name.","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"detail":"this model has no Production version","status":404,"title":"no production version","type":"about:blank"},"schema":{"properties":{"artifact_uri":{"description":"Artifact URI (omitempty).","type":"string"},"classification":{"description":"TBAC clearance ceiling (omitempty).","type":"string"},"created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"created_by":{"description":"Creator (omitempty).","type":"string"},"description":{"description":"Description (omitempty).","type":"string"},"framework":{"description":"Framework (omitempty).","type":"string"},"metrics":{"description":"Metrics (omitempty).","type":"string"},"model_name":{"description":"Parent model name.","type":"string"},"source_run_id":{"description":"Source run id (omitempty).","type":"string"},"stage":{"description":"Stage; always \"Production\" for this endpoint.","type":"string"},"tenant_id":{"description":"Owning tenant id.","type":"string"},"training_dataset_fqn":{"description":"Training dataset FQN (omitempty).","type":"string"},"version":{"description":"Version number.","type":"integer"}},"required":["tenant_id","model_name","version","stage","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the single ModelVersion currently in the Production stage for the named model (at most one Production version per tenant+model).","tags":["Registry"]}},"/api/v1/registry/models/{name}/transitions":{"get":{"description":"Returns the append-only stage-transition audit history for the named model across all its versions. Always 200 with a transitions array (never null); does not 404 for an unknown model — it simply returns an empty history.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"description":"Path parameter: the registered model name.","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"transitions":[]},"schema":{"properties":{"transitions":{"description":"Array of StageTransition audit entries; empty array when none.","items":{"type":"object"},"type":"array"},"transitions[].at":{"description":"RFC3339 timestamp of the transition.","type":"string"},"transitions[].by":{"description":"Actor who performed the transition (omitempty).","type":"string"},"transitions[].from":{"description":"Prior stage.","type":"string"},"transitions[].model_name":{"description":"Model name.","type":"string"},"transitions[].reason":{"description":"Free-text reason (omitempty).","type":"string"},"transitions[].tenant_id":{"description":"Owning tenant id.","type":"string"},"transitions[].to":{"description":"New stage.","type":"string"},"transitions[].version":{"description":"Version that moved.","type":"integer"}},"required":["transitions","transitions[].tenant_id","transitions[].model_name","transitions[].version","transitions[].from","transitions[].to","transitions[].at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the append-only stage-transition audit history for the named model across all its versions.","tags":["Registry"]}},"/api/v1/registry/models/{name}/versions":{"post":{"description":"Creates a new immutable, numbered version of the named model from a JSON body describing its artifact, provenance, metrics and classification. The version number and stage (None) are assigned by the registry. Returns 201 with the created ModelVersion; 400 on invalid body or a create error.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"artifact_uri":"s3://models/invoice-classifier/3","classification":"cui","created_by":"ml-team","description":"retrained on Q2 data","framework":"pytorch","metrics":{"accuracy":0.96,"f1":0.94},"source_run_id":"run_8842","training_dataset_fqn":"main.finance.invoices_train"},"schema":{"properties":{"artifact_uri":{"description":"URI of the stored model artifact.","type":"string"},"classification":{"description":"Highest data sensitivity cleared: unclassified|cui|il4|il5 (TBAC ceiling).","type":"string"},"created_by":{"description":"Creator identity.","type":"string"},"description":{"description":"Version description.","type":"string"},"framework":{"description":"Training/serving framework.","type":"string"},"metrics":{"description":"Evaluation metrics map.","type":"string"},"name":{"description":"Path parameter: the registered model name the version belongs to.","type":"string"},"source_run_id":{"description":"Originating training run id.","type":"string"},"training_dataset_fqn":{"description":"FQN of the training dataset.","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"artifact_uri":"s3://models/invoice-classifier/3","classification":"cui","created_at":"2026-07-15T12:00:00Z","created_by":"ml-team","description":"retrained on Q2 data","framework":"pytorch","metrics":{"accuracy":0.96,"f1":0.94},"model_name":"invoice-classifier","source_run_id":"run_8842","stage":"None","tenant_id":"acme","training_dataset_fqn":"main.finance.invoices_train","version":3},"schema":{"properties":{"artifact_uri":{"description":"Artifact URI (omitempty).","type":"string"},"classification":{"description":"TBAC clearance ceiling (omitempty).","type":"string"},"created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"created_by":{"description":"Creator (omitempty).","type":"string"},"description":{"description":"Description (omitempty).","type":"string"},"framework":{"description":"Framework (omitempty).","type":"string"},"metrics":{"description":"Metrics (omitempty).","type":"string"},"model_name":{"description":"Parent model name (from the path).","type":"string"},"source_run_id":{"description":"Source run id (omitempty).","type":"string"},"stage":{"description":"Initial stage; a new version starts as \"None\".","type":"string"},"tenant_id":{"description":"Owning tenant id (from tenantOf(r)).","type":"string"},"training_dataset_fqn":{"description":"Training dataset FQN (omitempty).","type":"string"},"version":{"description":"Registry-assigned version number.","type":"integer"}},"required":["tenant_id","model_name","version","stage","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates a new immutable, numbered version of the named model from a JSON body describing its artifact, provenance, metrics and classification.","tags":["Registry"]}},"/api/v1/registry/models/{name}/versions/{n}":{"get":{"description":"Fetches a single immutable ModelVersion by its number for the named model. Returns 400 if {n} is not a positive integer, and 404 if that version does not exist.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}},{"in":"path","name":"n","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"n":{"description":"Path parameter: version number; must be a positive integer (\u003e=1).","type":"integer"},"name":{"description":"Path parameter: the registered model name.","type":"string"}},"required":["name","n"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"detail":"version 1 not found","status":404,"title":"not found","type":"about:blank"},"schema":{"properties":{"artifact_uri":{"description":"Artifact URI (omitempty).","type":"string"},"classification":{"description":"TBAC clearance ceiling (omitempty).","type":"string"},"created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"created_by":{"description":"Creator (omitempty).","type":"string"},"description":{"description":"Description (omitempty).","type":"string"},"framework":{"description":"Framework (omitempty).","type":"string"},"metrics":{"description":"Metrics (omitempty).","type":"string"},"model_name":{"description":"Parent model name.","type":"string"},"source_run_id":{"description":"Source run id (omitempty).","type":"string"},"stage":{"description":"Lifecycle stage: None|Staging|Production|Archived.","type":"string"},"tenant_id":{"description":"Owning tenant id.","type":"string"},"training_dataset_fqn":{"description":"Training dataset FQN (omitempty).","type":"string"},"version":{"description":"Version number.","type":"integer"}},"required":["tenant_id","model_name","version","stage","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Fetches a single immutable ModelVersion by its number for the named model.","tags":["Registry"]}},"/api/v1/registry/models/{name}/versions/{n}/transition":{"post":{"description":"Transitions version {n} of the named model to a new lifecycle stage (None/Staging/Production/Archived), recording an audit entry. If the body omits 'by', the actor is resolved from the caller identity. Promoting to Production enforces the single-Production invariant (and auto-archives the prior Production version) in the adapter. Returns 200 with the updated ModelVersion.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}},{"in":"path","name":"n","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"by":"ml-lead","reason":"passed eval gate","to":"Production"},"schema":{"properties":{"by":{"description":"Actor performing the transition; if empty, resolved from the caller identity via resolveIdentity(r).","type":"string"},"n":{"description":"Path parameter: version number; must be a positive integer (\u003e=1).","type":"integer"},"name":{"description":"Path parameter: the registered model name.","type":"string"},"reason":{"description":"Free-text reason recorded in the audit trail.","type":"string"},"to":{"description":"Target stage: None|Staging|Production|Archived. Invalid targets are rejected by the adapter (=\u003e 400).","type":"string"}},"required":["name","n","to"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"artifact_uri":"s3://models/invoice-classifier/3","classification":"cui","created_at":"2026-07-15T12:00:00Z","created_by":"ml-team","framework":"pytorch","model_name":"invoice-classifier","stage":"Production","tenant_id":"acme","version":3},"schema":{"properties":{"artifact_uri":{"description":"Artifact URI (omitempty).","type":"string"},"classification":{"description":"TBAC clearance ceiling (omitempty).","type":"string"},"created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"created_by":{"description":"Creator (omitempty).","type":"string"},"description":{"description":"Description (omitempty).","type":"string"},"framework":{"description":"Framework (omitempty).","type":"string"},"metrics":{"description":"Metrics (omitempty).","type":"string"},"model_name":{"description":"Parent model name.","type":"string"},"source_run_id":{"description":"Source run id (omitempty).","type":"string"},"stage":{"description":"The new stage after the transition (equals the requested 'to').","type":"string"},"tenant_id":{"description":"Owning tenant id.","type":"string"},"training_dataset_fqn":{"description":"Training dataset FQN (omitempty).","type":"string"},"version":{"description":"Version number that was transitioned.","type":"integer"}},"required":["tenant_id","model_name","version","stage","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Transitions version {n} of the named model to a new lifecycle stage (None/Staging/Production/Archived), recording an audit entry.","tags":["Registry"]}},"/api/v1/report-subscriptions":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Report subscriptions"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Report subscriptions"],"x-stability":"experimental"}},"/api/v1/report-subscriptions/{id}":{"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Report subscriptions"],"x-stability":"experimental"},"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Report subscriptions"],"x-stability":"experimental"}},"/api/v1/report-subscriptions/{id}/deliver":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Report subscriptions"],"x-stability":"experimental"}},"/api/v1/review/items":{"get":{"description":"Serves the paged, filtered, faceted human-in-the-loop (HITL) review worklist. It reads the Iceberg gold table cedms.microfiche.pages (capped at 50000 rows), filters to pages that still need review (disposition human_review, doc_type unreadable/ocr_error, or any safety_flag; auto_accept/excluded pages are terminal and drop out), lazily reconciles a Postgres work-management overlay (routing new pages to a queue via the tenant's rules), applies facet and work-management filters, and returns one page plus Amazon-style facet counts. When no pages table exists yet it returns an empty queue (200) rather than erroring.","requestBody":{"content":{"application/json":{"schema":{"properties":{"assignee":{"description":"Exact-match filter on the overlay assignee.","type":"string"},"doctype":{"description":"Repeatable facet filter on doc_type (OR within the group).","items":{"type":"string"},"type":"array"},"limit":{"description":"Page size; defaults to 50, capped at 200; non-positive/garbage falls back to the default.","type":"integer"},"mine":{"description":"When '1', filters to items assigned to the calling subject (overrides assignee).","type":"string"},"offset":{"description":"Page window start; non-positive/garbage falls back to 0.","type":"integer"},"queue":{"description":"Exact-match filter on the overlay queue_id (Phase B work filter).","type":"string"},"reason":{"description":"Repeatable facet filter on the derived reason (OR within the group); e.g. reason=safety flag\u0026reason=unreadable.","items":{"type":"string"},"type":"array"},"state":{"description":"Exact-match filter on the overlay state (e.g. pending, in_progress).","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"facets":{"doc_type":[],"reason":[]},"items":[],"limit":50,"offset":0,"total":0},"schema":{"properties":{"facets":{"description":"Map with keys 'reason' and 'doc_type', each an array of {value,count} facet counts computed Amazon-style (each group counted over rows passing the other group's selection).","type":"object"},"items":{"description":"Page of review rows. Each row is a FLAT map of gold field name to string value (key, doc_type, confidence, disposition, safety_flag, ...) merged with overlay work fields (queue_id, assignee, state, reason, doctype_override, enqueued_at, due_at).","items":{"type":"object"},"type":"array"},"limit":{"description":"Echoes the effective (defaulted/capped) limit applied.","type":"integer"},"offset":{"description":"Echoes the effective offset applied.","type":"integer"},"total":{"description":"Count of items AFTER filtering (the pager's N), not the page size or table size.","type":"integer"}},"required":["items","total","offset","limit","facets"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Serves the paged, filtered, faceted human-in-the-loop (HITL) review worklist.","tags":["Review"]}},"/api/v1/review/items/{key}":{"patch":{"description":"Edits the reviewer-editable classification fields of an item — a doctype override and/or a reason override — the keyboard-speed correction write path (spec §5). At least one of doctype or reason must be provided. Returns the updated item DTO. NOT admin-gated.","parameters":[{"in":"path","name":"key","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"doctype":"correspondence","reason":"low confidence"},"schema":{"properties":{"doctype":{"description":"Reviewer's doctype override (pointer: only applied when present). Required if reason is absent.","type":"string"},"reason":{"description":"Reviewer's reason override (pointer). Required if doctype is absent.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"doctypeOverride":"correspondence","enqueuedAt":"2026-07-10T18:20:00Z","key":"cedms/microfiche/reel042/page0187","queueId":"48ccc403ef7cac92b60db16f96508e9f","reason":"low confidence","state":"in_progress"},"schema":{"properties":{"assignee":{"description":"Current assignee; omitted when empty.","type":"string"},"completedAt":{"description":"Completion time, RFC3339; omitted when unset.","type":"string"},"decision":{"description":"Terminal decision; omitted when empty.","type":"string"},"doctypeOverride":{"description":"Doctype override after update; omitted when empty.","type":"string"},"dueAt":{"description":"SLA due time, RFC3339; omitted when unset.","type":"string"},"enqueuedAt":{"description":"Enqueue timestamp, RFC3339.","type":"string"},"key":{"description":"Page key of the item.","type":"string"},"queueId":{"description":"Current queue id.","type":"string"},"reason":{"description":"Reason after update; omitted when empty.","type":"string"},"startedAt":{"description":"When work started, RFC3339; omitted when unset.","type":"string"},"state":{"description":"Lifecycle state.","type":"string"}},"required":["key","queueId","state","enqueuedAt"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Edits the reviewer-editable classification fields of an item — a doctype override and/or a reason override — the keyboard-speed correction write path (spec §5).","tags":["Review"]}},"/api/v1/review/items/{key}/assign":{"post":{"description":"Assigns a review item (identified by its page key) to a queue and/or a named assignee — the admin routing/hand-off action. At least one of queueId or assignee must be supplied. Returns the updated item DTO. Admin-only.","parameters":[{"in":"path","name":"key","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"assignee":"reviewer@example.com","queueId":"7740258cdd2d326397bfef2050283b57"},"schema":{"properties":{"assignee":{"description":"Assign the item to this subject. Required if queueId is empty.","type":"string"},"queueId":{"description":"Move the item to this queue. Required if assignee is empty.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"assignee":"reviewer@example.com","enqueuedAt":"2026-07-10T18:20:00Z","key":"cedms/microfiche/reel042/page0187","queueId":"7740258cdd2d326397bfef2050283b57","reason":"safety flag","state":"assigned"},"schema":{"properties":{"assignee":{"description":"Current assignee; omitted when empty.","type":"string"},"completedAt":{"description":"Completion time, RFC3339; omitted when unset.","type":"string"},"decision":{"description":"Terminal decision; omitted when empty.","type":"string"},"doctypeOverride":{"description":"Reviewer doctype override; omitted when empty.","type":"string"},"dueAt":{"description":"SLA due time, RFC3339; omitted when unset.","type":"string"},"enqueuedAt":{"description":"Enqueue timestamp, RFC3339.","type":"string"},"key":{"description":"Page key of the item.","type":"string"},"queueId":{"description":"Current queue id.","type":"string"},"reason":{"description":"Queue reason; omitted when empty.","type":"string"},"startedAt":{"description":"When work started, RFC3339; omitted when unset.","type":"string"},"state":{"description":"Lifecycle state.","type":"string"}},"required":["key","queueId","state","enqueuedAt"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Assigns a review item (identified by its page key) to a queue and/or a named assignee — the admin routing/hand-off action.","tags":["Review"]}},"/api/v1/review/items/{key}/claim":{"post":{"description":"Claims a review item for the calling subject (self-assign, the reviewer 'I'll take this' action). Take-once: if another reviewer already holds it, returns 409 Conflict. Returns the updated item DTO. NOT admin-gated — any authenticated reviewer may claim.","parameters":[{"in":"path","name":"key","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"assignee":"reviewer@example.com","enqueuedAt":"2026-07-10T18:20:00Z","key":"cedms/microfiche/reel042/page0187","queueId":"7740258cdd2d326397bfef2050283b57","reason":"safety flag","startedAt":"2026-07-10T18:25:00Z","state":"in_progress"},"schema":{"properties":{"assignee":{"description":"Now the calling subject; omitted when empty.","type":"string"},"completedAt":{"description":"Completion time, RFC3339; omitted when unset.","type":"string"},"decision":{"description":"Terminal decision; omitted when empty.","type":"string"},"doctypeOverride":{"description":"Reviewer doctype override; omitted when empty.","type":"string"},"dueAt":{"description":"SLA due time, RFC3339; omitted when unset.","type":"string"},"enqueuedAt":{"description":"Enqueue timestamp, RFC3339.","type":"string"},"key":{"description":"Page key of the item.","type":"string"},"queueId":{"description":"Current queue id.","type":"string"},"reason":{"description":"Queue reason; omitted when empty.","type":"string"},"startedAt":{"description":"When the claim/work started, RFC3339; omitted when unset.","type":"string"},"state":{"description":"Lifecycle state (e.g. in_progress after claim).","type":"string"}},"required":["key","queueId","state","enqueuedAt"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Claims a review item for the calling subject (self-assign, the reviewer 'I'll take this' action).","tags":["Review"]}},"/api/v1/review/queues":{"get":{"description":"Lists all review queues configured for the tenant. Queues are the routing targets for review items (e.g. a default 'Unassigned' catch-all plus team-owned queues like safety escalations or unreadable-pages), each with an owner, availability flag, optional SLA, and default marker.","responses":{"200":{"content":{"application/json":{"example":{"queues":[{"available":true,"createdAt":"2026-07-10T18:19:35Z","id":"2189e0fb8760791f6e0fb78a783aaf5e","isDefault":true,"name":"Unassigned","ownerRef":"review-admins","ownerType":"team"},{"available":true,"createdAt":"2026-07-10T18:18:39Z","id":"7740258cdd2d326397bfef2050283b57","isDefault":false,"name":"Safety escalations","ownerRef":"safety-team","ownerType":"team","slaHours":4},{"available":true,"createdAt":"2026-07-10T18:18:39Z","id":"48ccc403ef7cac92b60db16f96508e9f","isDefault":false,"name":"Unreadable pages","ownerRef":"ocr-team","ownerType":"team","slaHours":48}]},"schema":{"properties":{"queues":{"description":"Array of queue DTOs.","items":{"type":"object"},"type":"array"},"queues[].available":{"description":"Whether the queue currently accepts routing.","type":"boolean"},"queues[].coverageRef":{"description":"Optional coverage/assignment reference; omitted when empty.","type":"string"},"queues[].createdAt":{"description":"Creation timestamp, RFC3339.","type":"string"},"queues[].id":{"description":"Queue identifier.","type":"string"},"queues[].isDefault":{"description":"Whether this is the tenant's fallback/default queue.","type":"boolean"},"queues[].name":{"description":"Human-readable queue name.","type":"string"},"queues[].ownerRef":{"description":"Owner reference (e.g. a team slug).","type":"string"},"queues[].ownerType":{"description":"Owner kind (e.g. 'team', 'user').","type":"string"},"queues[].slaHours":{"description":"SLA target in hours; omitted when zero/unset.","type":"integer"}},"required":["queues","queues[].id","queues[].name","queues[].ownerType","queues[].ownerRef","queues[].available","queues[].isDefault","queues[].createdAt"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists all review queues configured for the tenant.","tags":["Review"]},"post":{"description":"Creates a new review queue for the tenant from a name, owner, optional coverage reference and optional SLA. Returns the created queue DTO with 201. Admin-only.","requestBody":{"content":{"application/json":{"example":{"coverageRef":"","name":"Safety escalations","ownerRef":"safety-team","ownerType":"team","slaHours":4},"schema":{"properties":{"coverageRef":{"description":"Optional coverage/assignment reference.","type":"string"},"name":{"description":"Queue display name; trimmed and must be non-empty (else 400).","type":"string"},"ownerRef":{"description":"Owner reference. Decoded but not validated by the handler.","type":"string"},"ownerType":{"description":"Owner kind (e.g. 'team'). Decoded but not validated by the handler despite the 400 message wording.","type":"string"},"slaHours":{"description":"Optional SLA target in hours.","type":"integer"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"available":true,"createdAt":"2026-07-10T18:18:39Z","id":"7740258cdd2d326397bfef2050283b57","isDefault":false,"name":"Safety escalations","ownerRef":"safety-team","ownerType":"team","slaHours":4},"schema":{"properties":{"available":{"description":"Availability flag (store default on create).","type":"boolean"},"coverageRef":{"description":"Coverage reference; omitted when empty.","type":"string"},"createdAt":{"description":"Creation timestamp, RFC3339.","type":"string"},"id":{"description":"New queue identifier.","type":"string"},"isDefault":{"description":"Whether this is the default queue.","type":"boolean"},"name":{"description":"Queue name (trimmed).","type":"string"},"ownerRef":{"description":"Owner reference as stored.","type":"string"},"ownerType":{"description":"Owner kind as stored.","type":"string"},"slaHours":{"description":"SLA hours; omitted when zero.","type":"integer"}},"required":["id","name","ownerType","ownerRef","available","isDefault","createdAt"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates a new review queue for the tenant from a name, owner, optional coverage reference and optional SLA.","tags":["Review"]}},"/api/v1/review/queues/{id}":{"patch":{"description":"Partially updates a review queue (name, coverage reference, availability, and/or SLA hours) identified by the path id. Uses pointer fields so only provided keys are changed. Returns the updated queue DTO. Admin-only.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"available":false,"slaHours":8},"schema":{"properties":{"available":{"description":"New availability flag (pointer).","type":"boolean"},"coverageRef":{"description":"New coverage reference (pointer).","type":"string"},"name":{"description":"New queue name (pointer: only applied when present).","type":"string"},"slaHours":{"description":"New SLA hours (pointer).","type":"integer"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"available":false,"createdAt":"2026-07-10T18:18:39Z","id":"48ccc403ef7cac92b60db16f96508e9f","isDefault":false,"name":"Unreadable pages","ownerRef":"ocr-team","ownerType":"team","slaHours":8},"schema":{"properties":{"available":{"description":"Availability flag after update.","type":"boolean"},"coverageRef":{"description":"Coverage reference; omitted when empty.","type":"string"},"createdAt":{"description":"Creation timestamp, RFC3339.","type":"string"},"id":{"description":"Queue identifier.","type":"string"},"isDefault":{"description":"Default marker.","type":"boolean"},"name":{"description":"Queue name after update.","type":"string"},"ownerRef":{"description":"Owner reference.","type":"string"},"ownerType":{"description":"Owner kind.","type":"string"},"slaHours":{"description":"SLA hours; omitted when zero.","type":"integer"}},"required":["id","name","ownerType","ownerRef","available","isDefault","createdAt"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Partially updates a review queue (name, coverage reference, availability, and/or SLA hours) identified by the path id.","tags":["Review"]}},"/api/v1/review/reviewed":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Review"],"x-stability":"experimental"}},"/api/v1/review/rules":{"get":{"description":"Returns the tenant's ordered routing rule set (the rules that map review-item attributes to a target queue) plus the current rules version. Array order is the priority order. Admin-only: reading rules requires the review-admin role.","responses":{"200":{"content":{"application/json":{"example":{"detail":"reading routing rules requires review-admin","status":403,"title":"forbidden","type":"about:blank"},"schema":{"properties":{"rules":{"description":"Ordered rule DTOs (index order = priority).","items":{"type":"object"},"type":"array"},"rules[].enabled":{"description":"Whether the rule is active.","type":"boolean"},"rules[].match":{"description":"Match predicate map (attribute -\u003e expected value) evaluated against item attrs (reason, docType, safetyFlag, confidence).","type":"object"},"rules[].queueId":{"description":"Target queue when the rule matches.","type":"string"},"version":{"description":"Opaque version token for the current rule set (error ignored; may be empty).","type":"string"}},"required":["rules","rules[].match","rules[].queueId","rules[].enabled"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the tenant's ordered routing rule set (the rules that map review-item attributes to a target queue) plus the current rules version.","tags":["Review"]},"put":{"description":"Replaces the tenant's WHOLE ordered routing rule set in one document (the rule builder edits it atomically); array order becomes the priority order (index+1). Every rule must carry a queueId. Returns 204 No Content on success. Admin-only.","requestBody":{"content":{"application/json":{"example":{"rules":[{"enabled":true,"match":{"reason":"safety flag"},"queueId":"7740258cdd2d326397bfef2050283b57"},{"enabled":true,"match":{"docType":"unreadable"},"queueId":"48ccc403ef7cac92b60db16f96508e9f"}]},"schema":{"properties":{"rules":{"description":"Full ordered rule list; array order IS priority. Each element is {match,queueId,enabled}.","items":{"type":"object"},"type":"array"},"rules[].enabled":{"description":"Whether the rule is active.","type":"boolean"},"rules[].match":{"description":"Match predicate map (attribute -\u003e expected value).","type":"object"},"rules[].queueId":{"description":"Target queue; every rule must set it (else 400 'every rule needs a queueId').","type":"string"}},"required":["rules","rules[].queueId"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Replaces the tenant's WHOLE ordered routing rule set in one document (the rule builder edits it atomically); array order becomes the priority order (index+1).","tags":["Review"]}},"/api/v1/rosters":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Rosters"],"x-stability":"experimental"}},"/api/v1/rosters/mine":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Rosters"],"x-stability":"experimental"}},"/api/v1/rosters/{section}":{"put":{"parameters":[{"in":"path","name":"section","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Rosters"],"x-stability":"experimental"}},"/api/v1/rulesets":{"get":{"description":"Lists every business-rules validation ruleset stored for the caller's tenant, sorted alphabetically by name. Each entry is the full stored ruleset object (name, rules array, and update metadata), not merely a name/count summary as the handler's doc comment suggests.","responses":{"200":{"content":{"application/json":{"example":{"rulesets":[]},"schema":{"properties":{"rulesets":{"description":"Array of ruleset objects (name, rules, updated_by, updated_at), sorted ascending by name; empty array when none exist.","items":{"type":"object"},"type":"array"}},"required":["rulesets"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists every business-rules validation ruleset stored for the caller's tenant, sorted alphabetically by name.","tags":["Rulesets"]}},"/api/v1/rulesets/{name}":{"delete":{"description":"Deletes a named ruleset for the caller's tenant. Idempotent: succeeds with 204 No Content whether or not the ruleset existed (the underlying object-store delete does not error on absence).","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"description":"Path parameter: ruleset name; must match the dot-joined [A-Za-z0-9_-] name rule, max 200 chars.","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes a named ruleset for the caller's tenant.","tags":["Rulesets"]},"get":{"description":"Returns a single named ruleset for the caller's tenant, including its full rules array and update metadata. Responds 404 when no ruleset with that name exists.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"description":"Path parameter: ruleset name; must be dot-joined segments of [A-Za-z0-9_-], max 200 chars.","type":"string"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"detail":"no ruleset named nonexistent","status":404,"title":"not found","type":"about:blank"},"schema":{"properties":{"name":{"description":"The ruleset name.","type":"string"},"rules":{"description":"The business rules in this ruleset (see PUT for the Rule shape).","items":{"type":"object"},"type":"array"},"updated_at":{"description":"RFC3339 UTC timestamp of the last write.","type":"string"},"updated_by":{"description":"Subject that last wrote the ruleset (omitted when empty).","type":"string"}},"required":["name","rules","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns a single named ruleset for the caller's tenant, including its full rules array and update metadata.","tags":["Rulesets"]},"put":{"description":"Creates or replaces a named ruleset for the caller's tenant from a JSON body of rules. Every rule is validated up front and a single malformed rule fails the whole request with 400 (no partial save); on success the stored ruleset (with server-set updated_by/updated_at) is returned.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"rules":[{"field":"amount","id":"amount-required","severity":"error","type":"required"},{"combine":"sum","id":"total-equation","operands":["subtotal","tax"],"severity":"warning","target":"total","tolerance":0.01,"type":"equation"}]},"schema":{"properties":{"rules":{"description":"Array of business rules to store. Each rule: id (string, required), type (string, required: required|regex|range|compare|allowed_values|equation), severity (string, required: info|warning|error), message (string, optional override), field (string; required for required/regex/range/compare/allowed_values), pattern (string; regex only, must compile), min/max (float pointers; range, at least one required), op (string eq|ne|gt|ge|lt|le; compare), value (float pointer; compare, required), allowed ([]string; allowed_values, non-empty), target (string; equation), combine (string sum|product|difference; equation), operands ([]string; equation, \u003e=1), tolerance (float; equation, default 0).","items":{"type":"object"},"type":"array"}},"required":["rules"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"name":"invoice-checks","rules":[{"field":"amount","id":"amount-required","severity":"error","type":"required"}],"updated_at":"2026-07-15T00:00:00Z","updated_by":"user:eng"},"schema":{"properties":{"name":{"description":"The ruleset name from the path.","type":"string"},"rules":{"description":"The stored rules, echoed back.","items":{"type":"object"},"type":"array"},"updated_at":{"description":"Server-set RFC3339 UTC write time.","type":"string"},"updated_by":{"description":"Subject of the request (omitted when empty).","type":"string"}},"required":["name","rules","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates or replaces a named ruleset for the caller's tenant from a JSON body of rules.","tags":["Rulesets"]}},"/api/v1/rulesets/{name}/validate":{"post":{"description":"Runs the named ruleset over a document's extracted fields and returns the findings plus an approval verdict. Each failing rule emits one finding at its configured severity; approval is granted (approved=true) unless the highest finding severity is error, so warnings and info are advisory only.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"fields":{"amount":"100.00","subtotal":"90.00","tax":"10.00","total":"100.00"}},"schema":{"properties":{"fields":{"description":"Extracted document fields keyed by field name, values as strings (numeric rules parse them, tolerating $/€/£, commas, whitespace, and parenthesized negatives). A null/omitted map is treated as empty.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"approved":false,"findings":[{"message":"amount is required but missing","rule_id":"amount-required","severity":"error"}],"highest_severity":"error","ruleset":"invoice-checks"},"schema":{"properties":{"approved":{"description":"False iff highest_severity is error; true otherwise (including no findings).","type":"boolean"},"findings":{"description":"One finding per failing rule: {rule_id (string), severity (info|warning|error), message (string)}. Empty array when nothing failed.","items":{"type":"object"},"type":"array"},"highest_severity":{"description":"Highest severity across findings (info|warning|error), or empty string when there are no findings.","type":"string"},"ruleset":{"description":"The ruleset name that was evaluated (from the path).","type":"string"}},"required":["ruleset","findings","highest_severity","approved"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs the named ruleset over a document's extracted fields and returns the findings plus an approval verdict.","tags":["Rulesets"]}},"/api/v1/search":{"post":{"description":"Runs hybrid semantic search over the microfiche corpus: it embeds the query via the Embedder and asks the VectorIndex for the k nearest pages, then also scans the gold table text for a verbatim (whitespace-normalized, case-insensitive) substring match, scoring those exact hits 1.0 and floating them above the semantic hits. Operator soft-deleted pages (disposition=excluded) are dropped from results at query time, and exact hits carry a snippet window around the matched phrase.","requestBody":{"content":{"application/json":{"example":{"k":10,"query":"protecting controlled unclassified information"},"schema":{"properties":{"k":{"description":"Number of nearest semantic hits to return; defaults to 10 when omitted or \u003c=0. Note: only bounds semantic hits — exact-substring matches are added on top and are not capped by k.","type":"integer"},"query":{"description":"Search string; trimmed, and a 400 is returned if empty.","type":"string"}},"required":["query"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"hits":[{"char_count":0,"doc_type":"nist_sp","exact":true,"key":"a0e2ac147019b11047ad1d0c9d1380db","score":1,"snippet":"Protecting Controlled Unclassified Information This publication provides requirements for protecting Controlled Unclassified Information…","source":"gov.documents.catalog","summary":"This publication provides requirements for protecting Controlled Unclassified Information (CUI) in nonfederal systems and organizations. It covers access control, audit and accountability, and other security requirements derived from NIST SP 800-53 controls. The requirements are intended for use by federal agencies in contracts or other agreements with nonfederal organizations that process, store, or transmit CUI.","tier":""}]},"schema":{"properties":{"hits":{"description":"Ranked results, exact (score 1.0) matches first, then semantic hits by descending similarity.","items":{"type":"object"},"type":"array"},"hits[].char_count":{"description":"Character count of the page text; may be 0 when not populated.","type":"integer"},"hits[].doc_type":{"description":"Classified document type stored at index time (e.g. nist_sp, omb_memo, nara_record).","type":"string"},"hits[].exact":{"description":"Present and true only on exact-substring matches; absent on pure semantic hits.","type":"boolean"},"hits[].key":{"description":"Stable page key (the vector index id / gold-table primary key).","type":"string"},"hits[].score":{"description":"Relevance score; 1.0 for exact-substring matches, otherwise the vector cosine similarity.","type":"number"},"hits[].snippet":{"description":"Text window (~90 chars each side) around the first occurrence of the query, with ellipses when truncated; present only on exact matches.","type":"string"},"hits[].summary":{"description":"LLM-generated page summary from the gold table.","type":"string"},"hits[].tier":{"description":"Sensitivity/importance tier stored at index time; empty string when unset.","type":"string"}},"required":["hits","hits[].key","hits[].score","hits[].doc_type","hits[].summary","hits[].tier","hits[].char_count"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs hybrid semantic search over the microfiche corpus: it embeds the query via the Embedder and asks the VectorIndex for the k nearest pages, then also scans the gold table text for a verbatim (whitespace-normalized, case-insensitive) substring match, scoring those exact hits 1.0 and floating them above the semantic hits.","tags":["Search"]}},"/api/v1/search/assist":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Search"],"x-stability":"experimental"}},"/api/v1/search/complete":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Search"],"x-stability":"experimental"}},"/api/v1/search/reindex":{"post":{"description":"Rebuilds the vector index from the existing gold table so a freshly started pod — whose in-memory index was wiped on restart — becomes searchable again without re-OCR'ing every page, provided the gold table still holds rows. It calls Microfiche.Reindex for the request tenant and returns the number of pages re-indexed.","responses":{"200":{"content":{"application/json":{"example":{"indexed":128},"schema":{"properties":{"indexed":{"description":"Number of gold-table pages re-embedded into the vector index.","type":"integer"}},"required":["indexed"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Rebuilds the vector index from the existing gold table so a freshly started pod — whose in-memory index was wiped on restart — becomes searchable again without re-OCR'ing every page, provided the gold table still holds rows.","tags":["Search"]}},"/api/v1/search/suggestions":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Data-grounded suggested queries (cached per tenant).","tags":["Search"],"x-stability":"experimental"}},"/api/v1/seeds":{"get":{"description":"Lists the calling tenant's version-controlled CSV seed definitions, sorted newest-updated first. Each entry carries the seed name, its destination table FQN, the last materialized row count, and load/update timestamps; the CSV body itself is omitted for brevity.","responses":{"200":{"content":{"application/json":{"example":{"seeds":[{"fqn":"gov.documents.catalog","last_loaded":"2026-07-08T22:14:53Z","name":"gov-documents-catalog","row_count":11,"updated_at":"2026-07-08T22:14:53Z"}]},"schema":{"properties":{"seeds":{"description":"Array of seed metadata objects, sorted by updated_at descending.","items":{"type":"object"},"type":"array"},"seeds[].fqn":{"description":"Fully-qualified destination table, catalog.schema.table.","type":"string"},"seeds[].last_loaded":{"description":"RFC3339 timestamp of the last successful materialize; omitted if never loaded.","type":"string"},"seeds[].name":{"description":"Seed identifier (the {name} path segment used on PUT/load/delete).","type":"string"},"seeds[].row_count":{"description":"Rows written by the most recent load (0 if never loaded).","type":"integer"},"seeds[].updated_at":{"description":"RFC3339 timestamp of the last definition write.","type":"string"}},"required":["seeds","seeds[].name","seeds[].fqn","seeds[].row_count","seeds[].updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists the calling tenant's version-controlled CSV seed definitions, sorted newest-updated first.","tags":["Seeds"]}},"/api/v1/seeds/{name}":{"delete":{"description":"Deletes a seed definition from the tenant's object store; the operation is idempotent (deleting a non-existent seed still succeeds). It removes only the stored definition and does NOT drop the previously materialized destination table.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes a seed definition from the tenant's object store; the operation is idempotent (deleting a non-existent seed still succeeds).","tags":["Seeds"]},"put":{"description":"Creates or replaces a seed definition (CSV content plus its destination catalog/schema/table). It validates that catalog.schema.table forms a valid identifier FQN and that the CSV is non-empty, then persists the definition to the tenant's object store; it does NOT materialize the table (call POST /seeds/{name}/load for that). If a seed with the same name already exists, its prior row_count and last_loaded are preserved.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"catalog":"gov","csv":"id,title\n1,Charter\n2,Bylaws","schema":"documents","table":"catalog"},"schema":{"properties":{"catalog":{"description":"Destination catalog; part of the validated FQN.","type":"string"},"csv":{"description":"CSV content of the seed; must be non-empty (whitespace-only rejected).","type":"string"},"schema":{"description":"Destination schema; part of the validated FQN.","type":"string"},"table":{"description":"Destination table name; part of the validated FQN.","type":"string"}},"required":["catalog","schema","table","csv"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"catalog":"gov","csv":"","name":"gov-documents-catalog","row_count":0,"schema":"documents","table":"catalog","updated_at":"2026-07-15T00:00:00Z","updated_by":"user@example.com"},"schema":{"properties":{"catalog":{"description":"Destination catalog.","type":"string"},"csv":{"description":"Always emitted empty — the CSV body is not echoed back.","type":"string"},"last_loaded":{"description":"Preserved last-load timestamp from a prior definition; omitted if never loaded.","type":"string"},"name":{"description":"Seed name (from the path).","type":"string"},"row_count":{"description":"Preserved row count from a prior definition, else 0.","type":"integer"},"schema":{"description":"Destination schema.","type":"string"},"table":{"description":"Destination table.","type":"string"},"updated_at":{"description":"RFC3339 timestamp set at write time.","type":"string"},"updated_by":{"description":"Subject (caller identity) that wrote this definition; omitted if empty.","type":"string"}},"required":["name","catalog","schema","table","csv","row_count","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates or replaces a seed definition (CSV content plus its destination catalog/schema/table).","tags":["Seeds"]}},"/api/v1/seeds/{name}/load":{"post":{"description":"Materializes a stored seed into its destination table with truncate-and-reload (REPLACE) semantics, so the table always reflects the committed CSV; the operation is idempotent. On success it registers the seeded table in the catalog if absent, invalidates the catalog graph, records the table in the durable object-store registry, and updates the seed's row_count and last_loaded before returning the load summary.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"fqn":"gov.documents.catalog","last_loaded":"2026-07-15T00:00:00Z","name":"gov-documents-catalog","rows_loaded":11},"schema":{"properties":{"fqn":{"description":"Destination table FQN that was loaded, catalog.schema.table.","type":"string"},"last_loaded":{"description":"RFC3339 timestamp of this load.","type":"string"},"name":{"description":"Seed name (from the path).","type":"string"},"rows_loaded":{"description":"Number of rows written by this load.","type":"integer"}},"required":["name","fqn","rows_loaded","last_loaded"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Materializes a stored seed into its destination table with truncate-and-reload (REPLACE) semantics, so the table always reflects the committed CSV; the operation is idempotent.","tags":["Seeds"]}},"/api/v1/shape/chat":{"post":{"description":"Mnemo on the Shape surface: given the current graph and a natural-language message, it asks the model (grounded in the tenant's readable catalog metadata) to return a full modified graph for the UI to diff and accept. If the model can't produce a valid graph — or no model is configured — it returns the unchanged graph plus an explanation, never a 500. Proposed graphs are sanitized so every operator has a unique id and dangling edges are dropped.","requestBody":{"content":{"application/json":{"example":{"graph":{"edges":[],"operators":[{"id":"src1","params":{"table":"main.weather.observations"},"position":{"x":0,"y":0},"type":"source"}]},"message":"group by station and compute max and min temperature","refs":["main.weather.observations"]},"schema":{"properties":{"graph":{"description":"Current v2 DAG Mnemo edits (preferred over recipe).","type":"object"},"message":{"description":"The natural-language request; must be non-empty.","type":"string"},"recipe":{"description":"Legacy v1 recipe, migrated when graph is empty.","type":"object"},"refs":{"description":"Referenced asset names injected into the prompt as extra grounding.","items":{"type":"string"},"type":"array"}},"required":["message"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"explanation":"Added a groupBy on station computing max and min temperature.","proposedGraph":{"edges":[{"from":"src1","fromPort":"out","to":"agg1","toPort":"in"}],"operators":[{"id":"src1","params":{"table":"main.weather.observations"},"position":{"x":0,"y":0},"type":"source"},{"id":"agg1","params":{"groupKeys":["station"],"measures":[{"as":"max_temp","col":"tmax_c","fn":"max"},{"as":"min_temp","col":"tmin_c","fn":"min"}]},"position":{"x":0,"y":0},"type":"groupBy"}]}},"schema":{"properties":{"explanation":{"description":"One or two sentences explaining the change (or why nothing changed).","type":"string"},"proposedGraph":{"description":"The full modified v2 graph to diff/accept (or the unchanged graph on failure).","type":"object"}},"required":["proposedGraph","explanation"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Mnemo on the Shape surface: given the current graph and a natural-language message, it asks the model (grounded in the tenant's readable catalog metadata) to return a full modified graph for the UI to diff and accept.","tags":["Shape"],"x-stability":"experimental"}},"/api/v1/shape/describe":{"post":{"description":"Returns a one-sentence, plain-language AI description of what a single operator does, grounded in its upstream columns. Degrades gracefully: with no inference model wired (or on model failure/empty output) it returns a deterministic rule-based summary of the operator, so it never 500s.","requestBody":{"content":{"application/json":{"example":{"operator":{"id":"flt1","params":{"conditions":[{"col":"population","op":"\u003e","value":500000000}],"match":"all"},"type":"filter"},"upstreamColumns":[{"name":"country","type":"string"},{"name":"population","type":"bigint"}]},"schema":{"properties":{"operator":{"description":"The operator to describe {id, type, params, position, version}.","type":"object"},"upstreamColumns":{"description":"Incoming columns [{name, type}] to ground the description.","items":{"type":"object"},"type":"array"}},"required":["operator"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"description":"Keeps only rows where population is greater than 500000000."},"schema":{"properties":{"description":{"description":"One plain-language sentence describing the operator (AI or deterministic fallback).","type":"string"}},"required":["description"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns a one-sentence, plain-language AI description of what a single operator does, grounded in its upstream columns.","tags":["Shape"],"x-stability":"experimental"}},"/api/v1/shape/preview":{"post":{"description":"Compiles the sub-DAG feeding a chosen node (or the terminal node when nodeId is empty) to Spark SQL, ReBAC-`read`-gates every referenced source table, tenantizes only the real source FQNs, runs the bounded query on Spark/Livy, and returns the resulting rows plus the node's output columns. When static column inference yields nothing but rows come back, columns are derived from the actual result rows.","requestBody":{"content":{"application/json":{"example":{"graph":{"edges":[{"from":"src1","fromPort":"out","to":"flt1","toPort":"in"}],"operators":[{"id":"src1","params":{"table":"demo.reference.population"},"position":{"x":0,"y":0},"type":"source"},{"id":"flt1","params":{"conditions":[{"col":"population","op":"\u003e","value":500000000}],"match":"all"},"position":{"x":220,"y":0},"type":"filter"}]},"limit":100,"nodeId":"flt1"},"schema":{"properties":{"graph":{"description":"v2 DAG to preview (preferred over recipe).","type":"object"},"limit":{"description":"Row cap for the bounded preview query (compiler-applied).","type":"integer"},"nodeId":{"description":"Operator id to preview up to; empty previews the terminal node.","type":"string"},"recipe":{"description":"Legacy v1 recipe, migrated when graph is empty.","type":"object"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"columns":[{"name":"country","type":"string"},{"name":"country_code","type":"string"},{"name":"population","type":"bigint"}],"rows":[{"country":"India","country_code":"IND","population":"1400000000"},{"country":"China","country_code":"CHN","population":"1410000000"}]},"schema":{"properties":{"columns":{"description":"Resulting columns [{name, type}]; type is \"\" when unknown or row-derived.","items":{"type":"object"},"type":"array"},"rows":{"description":"Result rows as flat key→value maps (all values JSON strings).","items":{"type":"object"},"type":"array"}},"required":["rows","columns"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Compiles the sub-DAG feeding a chosen node (or the terminal node when nodeId is empty) to Spark SQL, ReBAC-`read`-gates every referenced source table, tenantizes only the real source FQNs, runs the bounded query on Spark/Livy, and returns the resulting rows plus the node's output columns.","tags":["Shape"]}},"/api/v1/shape/publish":{"post":{"description":"Compiles the graph, ensures the target namespace, then materializes the result on Spark per mode — CREATE OR REPLACE VIEW (replace), CREATE TABLE IF NOT EXISTS + INSERT (append), or MERGE INTO on key columns (merge) — and registers the output in the catalog with a best-effort lineage edge from each source. Requires ReBAC `write` on the target namespace and `read` on every source table. The target may be given top-level or read from the graph's Output operator; env is accepted and echoed.","requestBody":{"content":{"application/json":{"example":{"env":"serverless","graph":{"edges":[{"from":"src1","fromPort":"out","to":"agg1","toPort":"in"},{"from":"agg1","fromPort":"out","to":"out1","toPort":"in"}],"operators":[{"id":"src1","params":{"table":"main.weather.observations"},"position":{"x":0,"y":0},"type":"source"},{"id":"agg1","params":{"groupKeys":["station"],"measures":[{"as":"max_temp","col":"tmax_c","fn":"max"}]},"position":{"x":220,"y":0},"type":"groupBy"},{"id":"out1","params":{"catalog":"main","mode":"replace","schema":"weather","table":"weather_extremes"},"position":{"x":440,"y":0},"type":"output"}]},"mode":"replace","target":{"catalog":"main","name":"weather_extremes","schema":"weather"}},"schema":{"properties":{"env":{"description":"Execution environment hint; accepted and echoed (warehouse selection is a no-op passthrough for now).","type":"string"},"graph":{"description":"v2 DAG to publish (preferred over recipe).","type":"object"},"keys":{"description":"Merge key columns; required (non-empty) when mode is merge.","items":{"type":"string"},"type":"array"},"mode":{"description":"replace | append | merge; defaults to replace. Also read from the Output operator when blank.","type":"string"},"recipe":{"description":"Legacy v1 recipe, migrated when graph is empty.","type":"object"},"target":{"description":"Explicit output target {catalog, schema, name}; falls back to the Output operator's params (name also falls back to the operator's `table` field) when any field is blank.","type":"object"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"columns":[{"name":"station","type":"string"},{"name":"max_temp","type":"double"}],"env":"serverless","kind":"view","mode":"replace","published":"main.weather.weather_extremes"},"schema":{"properties":{"columns":{"description":"Resulting output columns [{name, type}].","items":{"type":"object"},"type":"array"},"env":{"description":"Echoed env value from the request (always present, empty string when omitted).","type":"string"},"kind":{"description":"\"view\" (replace) or \"table\" (append/merge).","type":"string"},"mode":{"description":"Effective mode used (replace/append/merge).","type":"string"},"published":{"description":"Logical FQN of the published object (catalog.schema.name).","type":"string"}},"required":["published","kind","mode","env","columns"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Compiles the graph, ensures the target namespace, then materializes the result on Spark per mode — CREATE OR REPLACE VIEW (replace), CREATE TABLE IF NOT EXISTS + INSERT (append), or MERGE INTO on key columns (merge) — and registers the output in the catalog with a best-effort lineage edge from each source.","tags":["Shape"]}},"/api/v1/shape/recipes":{"get":{"description":"Lists all Shape recipes for the caller's tenant, newest first (sorted by updated_at descending). Each stored recipe is normalized so legacy v1 recipes surface as migrated v2 graphs.","responses":{"200":{"content":{"application/json":{"example":{"recipes":[{"graph":{"edges":[{"from":"src1","fromPort":"out","to":"flt1","toPort":"in"},{"from":"flt1","fromPort":"out","to":"srt1","toPort":"in"},{"from":"srt1","fromPort":"out","to":"out1","toPort":"in"}],"operators":[{"id":"src1","params":{"columns":[],"table":"demo.reference.population"},"position":{"x":0,"y":0},"type":"source","version":1},{"id":"flt1","params":{"conditions":[{"col":"population","op":"\u003e","value":500000000}],"match":"all"},"position":{"x":220,"y":0},"type":"filter","version":1},{"id":"srt1","params":{"col":"population","dir":"desc"},"position":{"x":440,"y":0},"type":"sort","version":1},{"id":"out1","params":{"catalog":"demo","mode":"append","schema":"reference","table":"large_countries"},"position":{"x":660,"y":0},"type":"output","version":1}]},"id":"6594b9d033e26647","name":"Large Countries by Population","updated_at":"2026-07-04T17:29:45Z","updated_by":"maya.analyst"}]},"schema":{"properties":{"recipes":{"description":"Array of stored recipes {id, name, graph, updated_by, updated_at}, newest first.","items":{"type":"object"},"type":"array"}},"required":["recipes"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists all Shape recipes for the caller's tenant, newest first (sorted by updated_at descending).","tags":["Shape"]},"post":{"description":"Persists a new Shape recipe (a data-prep DAG) to the tenant-scoped object store and returns it with a freshly generated hex id. Accepts either a v2 `graph` or a legacy v1 `recipe` (migrated to a graph on save); the name is required.","requestBody":{"content":{"application/json":{"example":{"graph":{"edges":[{"from":"src1","fromPort":"out","to":"out1","toPort":"in"}],"operators":[{"id":"src1","params":{"table":"demo.reference.population"},"position":{"x":0,"y":0},"type":"source"},{"id":"out1","params":{"catalog":"demo","mode":"append","schema":"reference","table":"large_countries"},"position":{"x":300,"y":0},"type":"output"}]},"name":"Large Countries by Population"},"schema":{"properties":{"graph":{"description":"v2 DAG {operators:[{id,type,params,position,version}], edges:[{from,fromPort,to,toPort}]}. Preferred over recipe.","type":"object"},"name":{"description":"Human-readable recipe name; must be non-empty after trimming.","type":"string"},"recipe":{"description":"Legacy v1 linear recipe {source_fqn, source_columns, steps}; migrated to a graph when graph is empty.","type":"object"}},"required":["name"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"graph":{"edges":[{"from":"src1","fromPort":"out","to":"out1","toPort":"in"}],"operators":[{"id":"src1","params":{"table":"demo.reference.population"},"position":{"x":0,"y":0},"type":"source"},{"id":"out1","params":{"catalog":"demo","mode":"append","schema":"reference","table":"large_countries"},"position":{"x":300,"y":0},"type":"output"}]},"id":"6594b9d033e26647","name":"Large Countries by Population","updated_at":"2026-07-04T17:29:45Z","updated_by":"maya.analyst"},"schema":{"properties":{"graph":{"description":"Resolved v2 graph (operators + edges).","type":"object"},"id":{"description":"Generated 16-char hex recipe id.","type":"string"},"name":{"description":"Trimmed recipe name.","type":"string"},"updated_at":{"description":"RFC3339 UTC timestamp.","type":"string"},"updated_by":{"description":"Subject that created it (\"anonymous\" when unauthenticated).","type":"string"}},"required":["id","name","graph","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Persists a new Shape recipe (a data-prep DAG) to the tenant-scoped object store and returns it with a freshly generated hex id.","tags":["Shape"]}},"/api/v1/shape/recipes/{id}":{"delete":{"description":"Deletes a Shape recipe by id from the tenant object store. Idempotent: it issues an unconditional object-store delete for the id's key and returns 204 whether or not the recipe existed.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the recipe id to delete.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes a Shape recipe by id from the tenant object store.","tags":["Shape"]},"get":{"description":"Fetches a single Shape recipe by id for the caller's tenant, returning it with any legacy v1 recipe migrated to a v2 graph. Returns 404 when no recipe with that id exists.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the recipe id.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"graph":{"edges":[{"from":"src1","fromPort":"out","to":"flt1","toPort":"in"},{"from":"flt1","fromPort":"out","to":"srt1","toPort":"in"},{"from":"srt1","fromPort":"out","to":"out1","toPort":"in"}],"operators":[{"id":"src1","params":{"columns":[],"table":"demo.reference.population"},"position":{"x":0,"y":0},"type":"source","version":1},{"id":"flt1","params":{"conditions":[{"col":"population","op":"\u003e","value":500000000}],"match":"all"},"position":{"x":220,"y":0},"type":"filter","version":1},{"id":"srt1","params":{"col":"population","dir":"desc"},"position":{"x":440,"y":0},"type":"sort","version":1},{"id":"out1","params":{"catalog":"demo","mode":"append","schema":"reference","table":"large_countries"},"position":{"x":660,"y":0},"type":"output","version":1}]},"id":"6594b9d033e26647","name":"Large Countries by Population","updated_at":"2026-07-04T17:29:45Z","updated_by":"maya.analyst"},"schema":{"properties":{"graph":{"description":"v2 DAG (operators + edges).","type":"object"},"id":{"description":"Recipe id.","type":"string"},"name":{"description":"Recipe name.","type":"string"},"updated_at":{"description":"RFC3339 UTC timestamp of last write.","type":"string"},"updated_by":{"description":"Subject of the last write.","type":"string"}},"required":["id","name","graph","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Fetches a single Shape recipe by id for the caller's tenant, returning it with any legacy v1 recipe migrated to a v2 graph.","tags":["Shape"]},"put":{"description":"Replaces an existing recipe's name and graph in place, preserving its id and stamping updated_by/updated_at. Loads the existing recipe first (404 if absent); a non-empty name overwrites the stored name while an empty name leaves it unchanged, and the graph is always replaced with the resolved body graph.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"graph":{"edges":[{"from":"src1","fromPort":"out","to":"out1","toPort":"in"}],"operators":[{"id":"src1","params":{"table":"demo.reference.population"},"position":{"x":0,"y":0},"type":"source"},{"id":"out1","params":{"catalog":"demo","mode":"replace","schema":"reference","table":"large_countries"},"position":{"x":300,"y":0},"type":"output"}]},"name":"Large Countries (v2)"},"schema":{"properties":{"graph":{"description":"Replacement v2 DAG; replaces the stored graph.","type":"object"},"id":{"description":"Path parameter: the recipe id to update.","type":"string"},"name":{"description":"New name; only applied when non-empty (blank keeps the existing name).","type":"string"},"recipe":{"description":"Legacy v1 recipe, migrated to a graph when graph is empty.","type":"object"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"graph":{"edges":[{"from":"src1","fromPort":"out","to":"out1","toPort":"in"}],"operators":[{"id":"src1","params":{"table":"demo.reference.population"},"position":{"x":0,"y":0},"type":"source"},{"id":"out1","params":{"catalog":"demo","mode":"replace","schema":"reference","table":"large_countries"},"position":{"x":300,"y":0},"type":"output"}]},"id":"6594b9d033e26647","name":"Large Countries (v2)","updated_at":"2026-07-15T00:00:00Z","updated_by":"maya.analyst"},"schema":{"properties":{"graph":{"description":"Replacement v2 graph.","type":"object"},"id":{"description":"Preserved recipe id.","type":"string"},"name":{"description":"Updated (or retained) name.","type":"string"},"updated_at":{"description":"RFC3339 UTC timestamp of this update.","type":"string"},"updated_by":{"description":"Subject of this update.","type":"string"}},"required":["id","name","graph","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Replaces an existing recipe's name and graph in place, preserving its id and stamping updated_by/updated_at.","tags":["Shape"]}},"/api/v1/shape/validate":{"post":{"description":"Runs pure, per-operator param/shape checks over the whole graph (known types, required params, input arity/disconnection, unknown columns via a lineage pass, and cycles) and returns every problem so the UI can mark cards errored and disable Run. Never touches compute; the handler always returns 200 with an errors array (empty when the graph is valid).","requestBody":{"content":{"application/json":{"example":{"graph":{"edges":[{"from":"src1","fromPort":"out","to":"out1","toPort":"in"}],"operators":[{"id":"src1","params":{"table":"demo.reference.population"},"position":{"x":0,"y":0},"type":"source"},{"id":"out1","params":{"catalog":"demo","mode":"append","schema":"reference","table":"large_countries"},"position":{"x":300,"y":0},"type":"output"}]}},"schema":{"properties":{"graph":{"description":"v2 DAG to validate (preferred over recipe).","type":"object"},"recipe":{"description":"Legacy v1 recipe, migrated when graph is empty.","type":"object"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"errors":[{"msg":"output operator needs a target table","opId":"out1"}]},"schema":{"properties":{"errors":{"description":"Validation problems [{opId, msg}]; opId \"\" means a graph-level error. Empty array when valid.","items":{"type":"object"},"type":"array"}},"required":["errors"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs pure, per-operator param/shape checks over the whole graph (known types, required params, input arity/disconnection, unknown columns via a lineage pass, and cycles) and returns every problem so the UI can mark cards errored and disable Run.","tags":["Shape"]}},"/api/v1/shares":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Shares"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Shares"],"x-stability":"experimental"}},"/api/v1/shares/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Shares"],"x-stability":"experimental"}},"/api/v1/shares/{id}/revoke":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Shares"],"x-stability":"experimental"}},"/api/v1/storage/buckets":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Storage"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Storage"],"x-stability":"experimental"}},"/api/v1/storage/buckets/{bucket}":{"get":{"parameters":[{"in":"path","name":"bucket","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Storage"],"x-stability":"experimental"}},"/api/v1/storage/buckets/{bucket}/owner":{"post":{"parameters":[{"in":"path","name":"bucket","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Storage"],"x-stability":"experimental"}},"/api/v1/subscriptions":{"get":{"description":"Lists the calling subject's own asset subscriptions, filtered server-side to the caller's identity and sorted newest-first by creation time. It scans the tenant's subscription estate and returns only records whose subscriber matches the caller.","responses":{"200":{"content":{"application/json":{"example":{"subscriptions":[]},"schema":{"properties":{"subscriptions":{"description":"Array of the caller's subscription objects (id, subscriber, table, events, created_at), newest first; empty array when the caller has none.","items":{"type":"object"},"type":"array"}},"required":["subscriptions"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists the calling subject's own asset subscriptions, filtered server-side to the caller's identity and sorted newest-first by creation time.","tags":["Subscriptions"]},"post":{"description":"Creates a subscription so the calling subject follows an individual catalog asset's change events (schema, deprecation, freshness, quality, ownership); when a matching asset-event is later published for that table, each subscriber gets a notification. The subscriber identity is taken from the request subject, not the body, and the call is ReBAC-gated so you can only subscribe to a table you are allowed to read.","requestBody":{"content":{"application/json":{"example":{"events":["schema","deprecation"],"table":"analytics.public.orders"},"schema":{"properties":{"events":{"description":"Event types to subscribe to; each must be one of schema, deprecation, freshness, quality, ownership. Omit or leave empty to receive all event types.","items":{"type":"string"},"type":"array"},"table":{"description":"Fully-qualified catalog.schema.table identifier of the asset to follow; must pass validTableFQN or the request is rejected 400.","type":"string"}},"required":["table"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"created_at":"2026-07-15T12:00:00Z","events":["schema","deprecation"],"id":"a1b2c3d4e5f60718","subscriber":"user:alice","table":"analytics.public.orders"},"schema":{"properties":{"created_at":{"description":"RFC3339 UTC creation timestamp.","type":"string"},"events":{"description":"The subscribed event filter as supplied; empty/absent means all event types.","items":{"type":"string"},"type":"array"},"id":{"description":"Server-generated 16-hex-char subscription id.","type":"string"},"subscriber":{"description":"Subject the subscription belongs to (derived from the caller, not the request body).","type":"string"},"table":{"description":"The subscribed table FQN.","type":"string"}},"required":["id","subscriber","table","created_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates a subscription so the calling subject follows an individual catalog asset's change events (schema, deprecation, freshness, quality, ownership); when a matching asset-event is later published for that table, each subscriber gets a notification.","tags":["Subscriptions"]}},"/api/v1/subscriptions/{id}":{"delete":{"description":"Deletes one of the caller's own subscriptions by id. It is idempotent and fails closed on ownership: an already-absent subscription returns 204, and a subscription owned by a different subject is rejected 403 rather than deleted.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the subscription id to delete (trimmed; empty yields 400).","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes one of the caller's own subscriptions by id.","tags":["Subscriptions"]}},"/api/v1/tables/{table}/certification":{"get":{"description":"Returns a table's certification / trust badge (draft, verified, or deprecated) plus optional note and decommission date (Atlan-certification / DataHub-deprecation parity, #224). Defaults to draft when unset.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"fqn":"isd.events.courses","status":"draft"},"schema":{"properties":{"certified_by":{"description":"Subject who last set certification","type":"string"},"decommission_date":{"description":"RFC3339 date, for deprecated assets","type":"string"},"fqn":{"description":"The table FQN","type":"string"},"note":{"description":"Free-text note (e.g. why deprecated)","type":"string"},"status":{"description":"draft | verified | deprecated","type":"string"},"updated_at":{"description":"RFC3339 last update","type":"string"}},"required":["fqn","status"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns a table's certification / trust badge (draft, verified, or deprecated) plus optional note and decommission date (Atlan-certification / DataHub-deprecation parity, #224).","tags":["Tables"]},"put":{"description":"Sets a table's certification status with an optional note and decommission date. Validates the status enum; a decommission_date must be RFC3339 and may only accompany a deprecated status.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"note":"reviewed by data-gov team","status":"verified"},"schema":{"properties":{"decommission_date":{"description":"RFC3339 date; only valid when status=deprecated","type":"string"},"note":{"description":"Free-text note","type":"string"},"status":{"description":"draft | verified | deprecated","type":"string"}},"required":["status"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"certified_by":"user:alice","fqn":"isd.events.courses","note":"reviewed by data-gov team","status":"verified","updated_at":"2026-07-15T00:00:00Z"},"schema":{"properties":{"certified_by":{"description":"Subject who saved","type":"string"},"decommission_date":{"description":"Saved decommission date","type":"string"},"fqn":{"description":"The table FQN","type":"string"},"note":{"description":"Saved note","type":"string"},"status":{"description":"The saved status","type":"string"},"updated_at":{"description":"RFC3339 save time","type":"string"}},"required":["fqn","status"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Sets a table's certification status with an optional note and decommission date.","tags":["Tables"]}},"/api/v1/tables/{table}/circuit-breaker":{"post":{"description":"Evaluates the table's data-quality expectations over its current rows and applies a breaker policy to decide whether to halt a pipeline run (Monte-Carlo-circuit-breaker parity). Trips if any check errored, any pass rate falls below min_pass_rate, or total failing rows exceed max_failing_rows.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"policy":{"max_failing_rows":0,"min_pass_rate":0.95}},"schema":{"properties":{"policy":{"description":"Breaker policy: min_pass_rate (float 0..1, 0 disables) and max_failing_rows (int, 0 disables). Omitting it (or a {} body) leaves both at 0 = disabled, so the breaker never trips. A request body is still required (empty body → 400).","type":"object"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"decision":{"action":"allow","checks_evaluated":0,"reasons":[],"tripped":false},"evaluated_at":"2026-07-15T00:00:00Z","fqn":"isd.events.courses","results":[],"rows_evaluated":48},"schema":{"properties":{"decision":{"description":"Verdict: tripped (bool), action (block|allow), reasons ([]string), checks_evaluated (int = len(results))","type":"object"},"evaluated_at":{"description":"RFC3339 evaluation time","type":"string"},"fqn":{"description":"The table FQN","type":"string"},"results":{"description":"Per-expectation results used for the decision","items":{"type":"object"},"type":"array"},"rows_evaluated":{"description":"Rows scored","type":"integer"}},"required":["fqn","rows_evaluated","decision","results","evaluated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Evaluates the table's data-quality expectations over its current rows and applies a breaker policy to decide whether to halt a pipeline run (Monte-Carlo-circuit-breaker parity).","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/classify-pii":{"post":{"description":"Samples up to 500 of the table's rows, classifies each column with a PII/PHI detector (ssn, email, phone, credit_card, dob), persists the non-empty column tags into the catalog-tag estate, and returns them (Unity-Catalog auto-classify parity).","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"classified_at":"2026-07-15T00:00:00Z","classified_by":"user:alice","fqn":"isd.events.courses","tags":[{"column":"email","match_rate":0.98,"tag":"pii:email"}]},"schema":{"properties":{"classified_at":{"description":"RFC3339 classification time","type":"string"},"classified_by":{"description":"Subject who ran classification","type":"string"},"fqn":{"description":"The table FQN","type":"string"},"tags":{"description":"Detected tags (sorted by column): column, tag (e.g. 'pii:ssn' / 'phi:dob'), match_rate","items":{"type":"object"},"type":"array"}},"required":["fqn","tags"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Samples up to 500 of the table's rows, classifies each column with a PII/PHI detector (ssn, email, phone, credit_card, dob), persists the non-empty column tags into the catalog-tag estate, and returns them (Unity-Catalog auto-classify parity).","tags":["Tables"]}},"/api/v1/tables/{table}/clone":{"post":{"description":"Clones a source table into ?to=\u003cdest fqn\u003e for dev/test branches (Snowflake-zero-copy-clone parity, #194). Currently a LOGICAL copy (reads source rows, writes them to the destination) — true metadata-only zero-copy awaits Iceberg snapshot sharing, honestly reported as mode:'logical-copy'.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"to":{"description":"Destination table FQN; must be valid, differ from source, and not already exist (query param)","type":"string"}},"required":["to"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"destination":"isd.dev.courses_copy","mode":"logical-copy","rows_cloned":48,"source":"isd.events.courses"},"schema":{"properties":{"destination":{"description":"The destination table FQN","type":"string"},"mode":{"description":"Always 'logical-copy' today (not true zero-copy)","type":"string"},"rows_cloned":{"description":"Number of rows copied","type":"integer"},"source":{"description":"The source table FQN","type":"string"}},"required":["source","destination","rows_cloned","mode"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Clones a source table into ?to=\u003cdest fqn\u003e for dev/test branches (Snowflake-zero-copy-clone parity, #194).","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/column-tags":{"get":{"description":"Returns the stored PII/PHI column tags for a table (empty when the table has not been classified yet).","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"fqn":"isd.events.courses","tags":[]},"schema":{"properties":{"classified_at":{"description":"RFC3339 last classification (omitted when unset)","type":"string"},"classified_by":{"description":"Subject who last classified (omitted when unset)","type":"string"},"fqn":{"description":"The table FQN","type":"string"},"tags":{"description":"Stored tags: column, tag, match_rate","items":{"type":"object"},"type":"array"}},"required":["fqn","tags"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the stored PII/PHI column tags for a table (empty when the table has not been classified yet).","tags":["Tables"]}},"/api/v1/tables/{table}/contract":{"get":{"description":"Returns a table's data contract — a producer-consumer agreement bundling a required schema, a freshness SLA, and quality expectations (#228). Responds 404 when no contract is set.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"detail":"isd.events.courses","status":404,"title":"no contract","type":"about:blank"},"schema":{"properties":{"contract":{"description":"Contract: schema ([]{name,type}), freshness ({warn_after_secs,error_after_secs}), quality ([]Expectation)","type":"object"},"fqn":{"description":"The table FQN","type":"string"}},"required":["fqn","contract"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns a table's data contract — a producer-consumer agreement bundling a required schema, a freshness SLA, and quality expectations (#228).","tags":["Tables"]},"put":{"description":"Sets (replaces) a table's data contract. Validated: the contract must assert at least one of schema, freshness, or quality; schema columns need names; freshness thresholds and quality expectations are each validated.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"freshness":{"error_after_secs":7200,"warn_after_secs":3600},"quality":[{"column":"status","type":"not_null"}],"schema":[{"name":"course_id","type":"string"}]},"schema":{"properties":{"freshness":{"description":"Freshness SLA: {warn_after_secs, error_after_secs}","type":"object"},"quality":{"description":"Quality expectations (same shape as /expectations)","items":{"type":"object"},"type":"array"},"schema":{"description":"Required columns: {name (req), type (optional; empty accepts any type)}","items":{"type":"object"},"type":"array"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"contract":{"freshness":{"error_after_secs":7200,"warn_after_secs":3600},"quality":[{"column":"status","type":"not_null"}],"schema":[{"name":"course_id","type":"string"}]},"fqn":"isd.events.courses"},"schema":{"properties":{"contract":{"description":"The saved contract","type":"object"},"fqn":{"description":"The table FQN","type":"string"}},"required":["fqn","contract"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Sets (replaces) a table's data contract.","tags":["Tables"]}},"/api/v1/tables/{table}/contract/verify":{"post":{"description":"Verifies a table against its stored contract in one call: schema (actual columns vs required), quality (expectation pass rates), and freshness (load-recency vs SLA), returning a combined report with an overall honored flag. 404 when no contract is set.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"fqn":"isd.events.courses","freshness":{"honored":true,"result":{"age_seconds":120,"state":"fresh"}},"honored":true,"quality":{"honored":true,"results":[]},"schema":{"honored":true,"violations":[]}},"schema":{"properties":{"fqn":{"description":"The table FQN","type":"string"},"freshness":{"description":"{result:{state,age_seconds}, honored:bool} — present only if contract has freshness","type":"object"},"honored":{"description":"Overall verdict (all present sections honored)","type":"boolean"},"quality":{"description":"{results:[Result], honored:bool} — present only if contract has quality","type":"object"},"schema":{"description":"{violations:[SchemaViolation], honored:bool} — present only if contract has a schema","type":"object"}},"required":["fqn","honored"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Verifies a table against its stored contract in one call: schema (actual columns vs required), quality (expectation pass rates), and freshness (load-recency vs SLA), returning a combined report with an overall honored flag.","tags":["Tables"]}},"/api/v1/tables/{table}/expectations":{"get":{"description":"Returns the data-quality expectation set defined on a table (#168). An undefined table simply has an empty expectation set (never an error).","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"expectations":[],"fqn":"isd.events.courses","updated_at":"","updated_by":""},"schema":{"properties":{"expectations":{"description":"Expectation objects: id, column, type (not_null|unique|range|allowed_values|regex), min, max, allowed, pattern","items":{"type":"object"},"type":"array"},"fqn":{"description":"The table FQN","type":"string"},"updated_at":{"description":"RFC3339 last-save time (empty when never set)","type":"string"},"updated_by":{"description":"Subject who last saved (empty when never set)","type":"string"}},"required":["fqn","expectations","updated_by","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the data-quality expectation set defined on a table (#168).","tags":["Tables"]},"put":{"description":"Replaces the entire expectation set on a table. Each expectation is validated for its type (range needs min or max, allowed_values needs a non-empty list, regex needs a compilable pattern, column is always required); any malformed one yields 400 with no partial save.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"expectations":[{"allowed":["OK","ERROR"],"column":"status","type":"allowed_values"},{"column":"duration_ms","min":0,"type":"range"}]},"schema":{"properties":{"expectations":{"description":"Full replacement set. Each: column (req), type (req: not_null|unique|range|allowed_values|regex), id, min, max, allowed[], pattern","items":{"type":"object"},"type":"array"}},"required":["expectations"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"expectations":[{"allowed":["OK","ERROR"],"column":"status","type":"allowed_values"},{"column":"duration_ms","min":0,"type":"range"}],"fqn":"isd.events.courses","updated_at":"2026-07-15T00:00:00Z","updated_by":"user:alice"},"schema":{"properties":{"expectations":{"description":"The saved expectation set (echoes the full submitted set)","items":{"type":"object"},"type":"array"},"fqn":{"description":"The table FQN","type":"string"},"updated_at":{"description":"RFC3339 save time","type":"string"},"updated_by":{"description":"Subject who saved","type":"string"}},"required":["fqn","expectations","updated_by","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Replaces the entire expectation set on a table.","tags":["Tables"]}},"/api/v1/tables/{table}/expectations/run":{"post":{"description":"Evaluates the table's stored expectations over its current rows (read via the governed Compute path) and returns a per-expectation scorecard plus an overall pass rate. With no expectations defined it short-circuits to an empty result with overall pass rate 1.0.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"fqn":"isd.events.courses","overall_pass_rate":1,"ran_at":"2026-07-15T00:00:00Z","results":[{"expectation":{"allowed":["OK"],"column":"status","type":"allowed_values"},"failing":0,"pass_rate":1,"passing":48,"total":48}],"rows_evaluated":48},"schema":{"properties":{"fqn":{"description":"The table FQN","type":"string"},"overall_pass_rate":{"description":"Mean of per-expectation pass rates ([0,1])","type":"number"},"ran_at":{"description":"RFC3339 run time (omitted on the empty-expectations short-circuit)","type":"string"},"results":{"description":"Per-expectation Result: expectation, total, passing, failing, pass_rate, error","items":{"type":"object"},"type":"array"},"rows_evaluated":{"description":"Number of rows scored; always present, 0 on the empty-expectations short-circuit","type":"integer"}},"required":["fqn","rows_evaluated","results","overall_pass_rate"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Evaluates the table's stored expectations over its current rows (read via the governed Compute path) and returns a per-expectation scorecard plus an overall pass rate.","tags":["Tables"]}},"/api/v1/tables/{table}/fields":{"get":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/fields/{field}/classify":{"post":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}},{"in":"path","name":"field","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/fields/{field}/declassify":{"post":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}},{"in":"path","name":"field","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/freshness":{"put":{"description":"Sets a table's warn/error freshness thresholds (in seconds). Validated: non-negative and warn \u003c= error when both are set. Preserves any existing loaded_at.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"config":{"error_after_secs":7200,"warn_after_secs":3600}},"schema":{"properties":{"config":{"description":"{warn_after_secs (int), error_after_secs (int)}; a 0 threshold disables that level","type":"object"}},"required":["config"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"config":{"error_after_secs":7200,"warn_after_secs":3600},"fqn":"isd.events.courses","updated_at":"2026-07-15T00:00:00Z","updated_by":"user:alice"},"schema":{"properties":{"config":{"description":"The saved thresholds","type":"object"},"fqn":{"description":"The table FQN","type":"string"},"loaded_at":{"description":"Preserved last-load time (omitted when unset)","type":"string"},"updated_at":{"description":"RFC3339 save time","type":"string"},"updated_by":{"description":"Subject who saved","type":"string"}},"required":["fqn","config"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Sets a table's warn/error freshness thresholds (in seconds).","tags":["Tables"]}},"/api/v1/tables/{table}/freshness/check":{"get":{"description":"Evaluates a table's load-recency against its warn/error thresholds and returns a fresh/warn/error/unknown verdict (dbt-source-freshness parity, #213). Unknown when no valid loaded_at is recorded.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"config":{"error_after_secs":0,"warn_after_secs":0},"fqn":"isd.events.courses","loaded_at":"","result":{"age_seconds":-1,"state":"unknown"}},"schema":{"properties":{"config":{"description":"Thresholds: {warn_after_secs, error_after_secs}","type":"object"},"fqn":{"description":"The table FQN","type":"string"},"loaded_at":{"description":"RFC3339 last-load time (empty when never marked)","type":"string"},"result":{"description":"Verdict: {state (fresh|warn|error|unknown), age_seconds (-1 when unknown)}","type":"object"}},"required":["fqn","config","loaded_at","result"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Evaluates a table's load-recency against its warn/error thresholds and returns a fresh/warn/error/unknown verdict (dbt-source-freshness parity, #213).","tags":["Tables"]}},"/api/v1/tables/{table}/freshness/loaded":{"post":{"description":"Bumps a table's loaded_at to now — the ingest path / vforce-flow pings this after a successful load so the freshness check has a recency anchor.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"config":{"error_after_secs":7200,"warn_after_secs":3600},"fqn":"isd.events.courses","loaded_at":"2026-07-15T00:00:00Z"},"schema":{"properties":{"config":{"description":"Existing thresholds (unchanged)","type":"object"},"fqn":{"description":"The table FQN","type":"string"},"loaded_at":{"description":"The new RFC3339 load time (now)","type":"string"},"updated_at":{"description":"Preserved from prior config save","type":"string"},"updated_by":{"description":"Preserved from prior config save","type":"string"}},"required":["fqn","config"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Bumps a table's loaded_at to now — the ingest path / vforce-flow pings this after a successful load so the freshness check has a recency anchor.","tags":["Tables"]}},"/api/v1/tables/{table}/impact":{"get":{"description":"Returns the lineage impact set of a table (#231): the transitive downstream blast radius (default) or upstream dependency set, computed by walking the catalog lineage graph. Impacted tables the subject cannot read are dropped from the result.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"direction":{"description":"downstream (default) | upstream (query param)","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"count":0,"direction":"downstream","impacted":[],"root":"isd.events.courses"},"schema":{"properties":{"count":{"description":"len(impacted)","type":"integer"},"direction":{"description":"Traversal direction used","type":"string"},"impacted":{"description":"Reachable table FQNs the subject can read","items":{"type":"string"},"type":"array"},"root":{"description":"The root table FQN","type":"string"}},"required":["root","direction","impacted","count"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the lineage impact set of a table (#231): the transitive downstream blast radius (default) or upstream dependency set, computed by walking the catalog lineage graph.","tags":["Tables"]}},"/api/v1/tables/{table}/mask-policies":{"get":{"description":"Returns the column-masking policies on a table: a map of column name to mask policy (redact / hash / partial) applied at the rows-read path for principals lacking `unmask` (Unity-Catalog column-masking parity, #173). Empty map when none.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"fqn":"isd.events.courses","policies":{}},"schema":{"properties":{"fqn":{"description":"The table FQN","type":"string"},"policies":{"description":"column → {type (redact|hash|partial)}","type":"string"},"updated_at":{"description":"RFC3339 last save","type":"string"},"updated_by":{"description":"Subject who last saved","type":"string"}},"required":["fqn","policies"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the column-masking policies on a table: a map of column name to mask policy (redact / hash / partial) applied at the rows-read path for principals lacking `unmask` (Unity-Catalog column-masking parity, #173).","tags":["Tables"]},"put":{"description":"Replaces the column-masking policy set on a table. Defining masking is a governance action gated on `unmask` (only a principal who can see the data unmasked may decide how others see it). Each policy's type is validated.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"policies":{"subject":{"type":"partial"},"trace_id":{"type":"hash"}}},"schema":{"properties":{"policies":{"description":"column → {type: redact|hash|partial} (empty type = no masking)","type":"string"}},"required":["policies"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"fqn":"isd.events.courses","policies":{"subject":{"type":"partial"},"trace_id":{"type":"hash"}},"updated_at":"2026-07-15T00:00:00Z","updated_by":"user:alice"},"schema":{"properties":{"fqn":{"description":"The table FQN","type":"string"},"policies":{"description":"The saved policy map","type":"string"},"updated_at":{"description":"RFC3339 save time","type":"string"},"updated_by":{"description":"Subject who saved","type":"string"}},"required":["fqn","policies"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Replaces the column-masking policy set on a table.","tags":["Tables"]}},"/api/v1/tables/{table}/materialize":{"post":{"description":"Runs a model's SELECT and materializes the result into {table} using a strategy — append, insert_overwrite, or merge (upsert by unique_key) — dbt-incremental-model parity (#208), distinct from a full MV refresh.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"sql":"SELECT * FROM isd.events.courses","strategy":"merge","unique_key":"id"},"schema":{"properties":{"sql":{"description":"The model SELECT (must target a single table, e.g. SELECT * FROM \u003cfqn\u003e)","type":"string"},"strategy":{"description":"append | insert_overwrite | merge","type":"string"},"unique_key":{"description":"Upsert key (required by the merge strategy)","type":"string"}},"required":["sql","strategy"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"incoming":48,"strategy":"merge","target":"isd.events.courses_daily","total_rows":48},"schema":{"properties":{"incoming":{"description":"Rows produced by the model SELECT","type":"integer"},"strategy":{"description":"The strategy applied","type":"string"},"target":{"description":"The target table FQN","type":"string"},"total_rows":{"description":"Rows in the target after merge","type":"integer"}},"required":["target","strategy","incoming","total_rows"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs a model's SELECT and materializes the result into {table} using a strategy — append, insert_overwrite, or merge (upsert by unique_key) — dbt-incremental-model parity (#208), distinct from a full MV refresh.","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/ownership":{"get":{"description":"Returns a table's ownership record: typed owners (technical / business / steward / expert) and an optional domain (DataHub-owners-domains parity, #227). Empty owners when unset.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"fqn":"isd.events.courses","owners":[]},"schema":{"properties":{"domain":{"description":"The asset's domain (omitted when unset)","type":"string"},"fqn":{"description":"The table FQN","type":"string"},"owners":{"description":"Owners: {principal, role (technical|business|steward|expert)}","items":{"type":"object"},"type":"array"},"updated_at":{"description":"RFC3339 last save","type":"string"},"updated_by":{"description":"Subject who last saved","type":"string"}},"required":["fqn","owners"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns a table's ownership record: typed owners (technical / business / steward / expert) and an optional domain (DataHub-owners-domains parity, #227).","tags":["Tables"]},"put":{"description":"Sets a table's owners and domain. Each owner needs a non-empty principal and a valid role (technical / business / steward / expert).","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"domain":"learning","owners":[{"principal":"user:alice","role":"steward"}]},"schema":{"properties":{"domain":{"description":"The asset's domain","type":"string"},"owners":{"description":"[{principal (req), role (req: technical|business|steward|expert)}]","items":{"type":"object"},"type":"array"}},"required":["owners"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"domain":"learning","fqn":"isd.events.courses","owners":[{"principal":"user:alice","role":"steward"}],"updated_at":"2026-07-15T00:00:00Z","updated_by":"user:alice"},"schema":{"properties":{"domain":{"description":"The saved domain","type":"string"},"fqn":{"description":"The table FQN","type":"string"},"owners":{"description":"The saved owners","items":{"type":"object"},"type":"array"},"updated_at":{"description":"RFC3339 save time","type":"string"},"updated_by":{"description":"Subject who saved","type":"string"}},"required":["fqn","owners"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Sets a table's owners and domain.","tags":["Tables"]}},"/api/v1/tables/{table}/quality/failing-records":{"get":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/quality/failing-records/export":{"get":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/quality/run-async":{"post":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/quality/run-async/{id}/stream":{"get":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/quality/runs":{"get":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/quality/suggest-pattern":{"post":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/quality/test-pattern":{"post":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/readme":{"get":{"description":"Returns an asset's canonical README — markdown body plus linked external resources (Slack, Jira, Confluence, GitHub, link) (Atlan-README parity, #235). Empty body and resources when unset.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"body":"","fqn":"isd.events.courses","resources":[]},"schema":{"properties":{"body":{"description":"Markdown body","type":"string"},"fqn":{"description":"The table FQN","type":"string"},"resources":{"description":"External links: {label, url, type}","items":{"type":"object"},"type":"array"},"updated_at":{"description":"RFC3339 last save","type":"string"},"updated_by":{"description":"Subject who last saved","type":"string"}},"required":["fqn","body","resources"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns an asset's canonical README — markdown body plus linked external resources (Slack, Jira, Confluence, GitHub, link) (Atlan-README parity, #235).","tags":["Tables"]},"put":{"description":"Sets an asset's README body and resource links. Editing the canonical doc is a management action gated on `unmask` (not just read). Each resource needs a label, a recognized type, and an absolute http(s) URL (javascript:/data:/file: rejected as XSS vectors).","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"body":"# Courses\nISD course-authored events.","resources":[{"label":"Runbook","type":"confluence","url":"https://wiki.example.com/courses"}]},"schema":{"properties":{"body":{"description":"Markdown body (max 100000 bytes)","type":"string"},"resources":{"description":"[{label (req), url (req; must be absolute http/https), type (slack|jira|confluence|github|link, default link)}]","items":{"type":"object"},"type":"array"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"body":"# Courses\nISD course-authored events.","fqn":"isd.events.courses","resources":[{"label":"Runbook","type":"confluence","url":"https://wiki.example.com/courses"}],"updated_at":"2026-07-15T00:00:00Z","updated_by":"user:alice"},"schema":{"properties":{"body":{"description":"The saved body","type":"string"},"fqn":{"description":"The table FQN","type":"string"},"resources":{"description":"The saved resources (type normalized to lowercase)","items":{"type":"object"},"type":"array"},"updated_at":{"description":"RFC3339 save time","type":"string"},"updated_by":{"description":"Subject who saved","type":"string"}},"required":["fqn","body","resources"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Sets an asset's README body and resource links.","tags":["Tables"]}},"/api/v1/tables/{table}/reviews":{"delete":{"description":"Removes the caller's OWN review of a table (idempotent). A principal can never delete another's review because the storage key is derived from the caller's own subject.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Removes the caller's OWN review of a table (idempotent).","tags":["Tables"]},"get":{"description":"Returns all reviews for a table plus the aggregate trust signal (count and one-decimal average rating). Reviews are crowd-sourced 1-5 ratings with optional comments (Collibra-Data-Marketplace parity, #237), sorted by author.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"average_rating":0,"count":0,"fqn":"isd.events.courses","reviews":[]},"schema":{"properties":{"average_rating":{"description":"Mean rating to one decimal (0 when none)","type":"number"},"count":{"description":"Number of reviews","type":"integer"},"fqn":{"description":"The table FQN","type":"string"},"reviews":{"description":"Reviews: {fqn, author, rating, comment, updated_at}","items":{"type":"object"},"type":"array"}},"required":["fqn","reviews","count","average_rating"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns all reviews for a table plus the aggregate trust signal (count and one-decimal average rating).","tags":["Tables"]},"post":{"description":"Submits or updates the CALLER's review of a table. The author is always the authenticated subject (never taken from the body), rating must be 1-5, and each principal has at most one review per asset (a re-POST overwrites).","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"comment":"Reliable and well documented.","rating":5},"schema":{"properties":{"comment":{"description":"Free-text comment (max 2000 chars)","type":"string"},"rating":{"description":"Star rating 1..5","type":"integer"}},"required":["rating"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"author":"user:alice","comment":"Reliable and well documented.","fqn":"isd.events.courses","rating":5,"updated_at":"2026-07-15T00:00:00Z"},"schema":{"properties":{"author":{"description":"The authenticated subject","type":"string"},"comment":{"description":"The saved comment (trimmed)","type":"string"},"fqn":{"description":"The table FQN","type":"string"},"rating":{"description":"The saved rating","type":"integer"},"updated_at":{"description":"RFC3339 save time","type":"string"}},"required":["fqn","author","rating","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Submits or updates the CALLER's review of a table.","tags":["Tables"]}},"/api/v1/tables/{table}/rows":{"get":{"description":"Reads rows from a table (FQN in the path), capped and streamed so the server never materializes the whole table. Applies the deactivation-suppression gate (404 mid-drop), the ReBAC read gate, column masking for principals lacking `unmask`, and falls back to the distributed (Spark/Livy) engine for Spark views the embedded engine can't read.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"limit":{"description":"Max rows to return; default 1000, hard-capped at 50000 (query param)","type":"integer"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"limit":1,"rows":[{"data":"{\"course_id\":\"COURSE-0005\",\"duration_ms\":101,\"otel_kind\":\"PRODUCER\",\"parent_span_id\":\"49dd043126b3ac2e\",\"source_system\":\"vforce-isd\",\"span_id\":\"4b74f2ea2f7ae075\",\"status\":\"OK\",\"tenant_id\":\"demo\",\"trace_id\":\"d9e598e5cb1d6d2ff99aadfd6bf76b3d\"}","datacontenttype":"","id":"span-0005-2","source":"vforce-isd","specversion":"1.0","subject":"txn-0005","tenant_id":"demo","time":"2026-06-06T05:10:00Z","traceparent":"00-d9e598e5cb1d6d2ff99aadfd6bf76b3d-4b74f2ea2f7ae075-01","tracestate":"vforce=s:2","type":"com.ltm.workforce.isd.course.authored"}],"total":48,"truncated":true},"schema":{"properties":{"limit":{"description":"The effective limit applied","type":"integer"},"rows":{"description":"Rows as flat field maps (domain.Row marshals its Values map directly)","items":{"type":"object"},"type":"array"},"total":{"description":"Total row count known to the read (may exceed len(rows) when truncated)","type":"integer"},"truncated":{"description":"True when more rows exist beyond the returned page","type":"boolean"}},"required":["rows","total","truncated","limit"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Reads rows from a table (FQN in the path), capped and streamed so the server never materializes the whole table.","tags":["Catalog"]}},"/api/v1/tables/{table}/snapshot":{"post":{"description":"Takes a point-in-time SCD2 snapshot of a table keyed by ?key= (#210): reads the live source rows, merges into the prior snapshot history (closing changed/deleted versions, opening new ones), and writes the result to a sibling '\u003ctable\u003e__snapshot' table. Returns version counts.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"key":{"description":"The change-tracking key column (must exist in the source) (query param)","type":"string"}},"required":["key"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"current_versions":48,"key":"id","snapshot":"isd.events.courses__snapshot","source":"isd.events.courses","total_versions":48},"schema":{"properties":{"current_versions":{"description":"Rows whose __is_current=='true'","type":"integer"},"key":{"description":"The change-tracking key used","type":"string"},"snapshot":{"description":"The derived snapshot table FQN (\u003ctable\u003e__snapshot)","type":"string"},"source":{"description":"The source table FQN","type":"string"},"total_versions":{"description":"Total row versions in the merged history","type":"integer"}},"required":["source","snapshot","key","total_versions","current_versions"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Takes a point-in-time SCD2 snapshot of a table keyed by ?key= (#210): reads the live source rows, merges into the prior snapshot history (closing changed/deleted versions, opening new ones), and writes the result to a sibling '\u003ctable\u003e__snapshot' table.","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/suppression-policy":{"get":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"},"put":{"parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tables"],"x-stability":"experimental"}},"/api/v1/tables/{table}/tests":{"get":{"description":"Returns the dbt-style transformation tests defined on a model (#209): named SQL assertions where the query selects the FAILING rows (zero rows = pass). Results are sorted by name.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"model":"isd.events.courses","tests":[]},"schema":{"properties":{"model":{"description":"The model (table FQN)","type":"string"},"tests":{"description":"Tests: {name, sql}","items":{"type":"object"},"type":"array"},"updated_at":{"description":"RFC3339 last save","type":"string"},"updated_by":{"description":"Subject who last saved","type":"string"}},"required":["model","tests"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the dbt-style transformation tests defined on a model (#209): named SQL assertions where the query selects the FAILING rows (zero rows = pass).","tags":["Tables"]},"put":{"description":"Replaces the transformation-test set on a model. Each test requires a non-empty name and non-empty SQL.","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"tests":[{"name":"no_null_status","sql":"SELECT * FROM isd.events.courses WHERE status IS NULL"}]},"schema":{"properties":{"tests":{"description":"Full replacement set: [{name (req), sql (req; should select failing rows)}]","items":{"type":"object"},"type":"array"}},"required":["tests"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"model":"isd.events.courses","tests":[{"name":"no_null_status","sql":"SELECT * FROM isd.events.courses WHERE status IS NULL"}],"updated_at":"2026-07-15T00:00:00Z","updated_by":"user:alice"},"schema":{"properties":{"model":{"description":"The model (table FQN)","type":"string"},"tests":{"description":"The saved tests","items":{"type":"object"},"type":"array"},"updated_at":{"description":"RFC3339 save time","type":"string"},"updated_by":{"description":"Subject who saved","type":"string"}},"required":["model","tests"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Replaces the transformation-test set on a model.","tags":["Tables"]}},"/api/v1/tables/{table}/tests/run":{"post":{"description":"Executes every transformation test on a model and returns binary pass/fail plus a failing-row count per test. A test passes when its SQL returns zero rows; a SQL error is a test error (not a pass).","parameters":[{"in":"path","name":"table","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"all_passed":true,"model":"isd.events.courses","results":[{"failing_rows":0,"name":"no_null_status","passed":true}],"test_count":1},"schema":{"properties":{"all_passed":{"description":"True iff every test passed","type":"boolean"},"model":{"description":"The model (table FQN)","type":"string"},"results":{"description":"Per-test: {name, passed, failing_rows, error}","items":{"type":"object"},"type":"array"},"test_count":{"description":"Number of tests run","type":"integer"}},"required":["model","all_passed","test_count","results"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Executes every transformation test on a model and returns binary pass/fail plus a failing-row count per test.","tags":["Tables"]}},"/api/v1/tattoos":{"get":{"description":"Returns a page of the caller's tenant tattoo gallery (each record is just an id and its viewer image_url) for the 'pick an existing tattoo' grid, plus the total count. Pagination is backend-enforced with a default page of 48 and a hard cap of 200 per request.","requestBody":{"content":{"application/json":{"schema":{"properties":{"limit":{"description":"Page size; defaults to 48, values \u003e200 are clamped to 200, non-positive/invalid ignored.","type":"integer"},"offset":{"description":"Zero-based row offset into the gallery; non-positive/invalid treated as 0.","type":"integer"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"limit":2,"offset":0,"tattoos":[{"id":"drozdik-v0-00000","image_url":"/api/v1/tattoos/drozdik-v0-00000/image","score":0},{"id":"drozdik-v0-00001","image_url":"/api/v1/tattoos/drozdik-v0-00001/image","score":0}],"total":4700},"schema":{"properties":{"limit":{"description":"The effective (clamped) limit applied to this page.","type":"integer"},"offset":{"description":"The effective offset applied to this page.","type":"integer"},"tattoos":{"description":"Gallery records for this page; each has id and image_url (score is 0, labels/image_key/ingested_at omitted on the list projection).","items":{"type":"object"},"type":"array"},"total":{"description":"Total number of tattoos in the tenant gallery across all pages.","type":"integer"}},"required":["tattoos","total","offset","limit"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns a page of the caller's tenant tattoo gallery (each record is just an id and its viewer image_url) for the 'pick an existing tattoo' grid, plus the total count.","tags":["Tattoos"]}},"/api/v1/tattoos/enroll":{"post":{"description":"Async intake path used by the browser enrollment FORM: it validates the id and image, creates a forensics job, publishes the enrolled event, and returns 202 immediately while the heavy embed→store→index runs out of band; the client then polls or streams SSE progress via the /api/v1/forensics/jobs/{id} endpoints. Structured intake metadata (case_id, subject, etc.) rides on the job into the vector payload.","requestBody":{"content":{"application/json":{"example":{"body_location":"left forearm","case_id":"CASE-42","data":"data:image/png;base64,iVBORw0KGgo...","id":"tat-001","labels":["tribal","sleeve"],"notes":"observed at intake","subject":"John Doe"},"schema":{"properties":{"age_range":{"description":"Optional intake metadata (primarily a faces field); stored in the vector payload when non-empty.","type":"string"},"body_location":{"description":"Optional body-location descriptor for the tattoo; stored when non-empty.","type":"string"},"case_id":{"description":"Optional investigative case id; stored in the vector payload when non-empty.","type":"string"},"data":{"description":"Base64 image bytes or a data: URL (e.g. 'data:image/png;base64,...'); an empty/invalid payload is a 400.","type":"string"},"id":{"description":"Subject/case id for the enrollment; trimmed, and rejected (400) if empty.","type":"string"},"labels":{"description":"Free-form tags stored with the record and returned on search hits.","items":{"type":"string"},"type":"array"},"notes":{"description":"Optional free-text notes; stored in the vector payload when non-empty.","type":"string"},"subject":{"description":"Optional subject identifier; stored in the vector payload when non-empty.","type":"string"}},"required":["id","data"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"job_id":"\u003cgenerated\u003e","kind":"tattoo","phase":"queued","record_id":"tat-001"},"schema":{"properties":{"job_id":{"description":"Generated id for the async enrollment job; poll GET /api/v1/forensics/jobs/{id} or stream .../stream with it.","type":"string"},"kind":{"description":"Biometric kind; always 'tattoo' for this route.","type":"string"},"phase":{"description":"Initial job phase; always 'queued'.","type":"string"},"record_id":{"description":"The (trimmed) id that will become the gallery record once indexed.","type":"string"}},"required":["job_id","record_id","kind","phase"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Async intake path used by the browser enrollment FORM: it validates the id and image, creates a forensics job, publishes the enrolled event, and returns 202 immediately while the heavy embed→store→index runs out of band; the client then polls or streams SSE progress via the /api/v1/forensics/jobs/{id} endpoints.","tags":["Tattoos"]}},"/api/v1/tattoos/ingest":{"post":{"description":"Synchronous, back-compat indexing path that embeds the tattoo image, stores its bytes in the object store, and upserts the vector into the Qdrant index in-band, returning the created gallery record. Re-ingesting the same id upserts idempotently; the browser intake form uses the async /enroll route instead.","requestBody":{"content":{"application/json":{"example":{"data":"data:image/png;base64,iVBORw0KGgo...","id":"tat-001","labels":["tribal","sleeve"]},"schema":{"properties":{"data":{"description":"Base64 image bytes or a data: URL; empty/invalid payload is a 400 'invalid image'.","type":"string"},"id":{"description":"Record id; trimmed before use and rejected by the service if empty.","type":"string"},"labels":{"description":"Free-form tags stored with the record.","items":{"type":"string"},"type":"array"}},"required":["id","data"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"id":"tat-001","image_key":"tattoos/tat-001.png","image_url":"/api/v1/tattoos/tat-001/image","ingested_at":"2026-07-15T12:00:00Z","labels":["tribal","sleeve"],"score":0},"schema":{"properties":{"id":{"description":"The ingested record id.","type":"string"},"image_key":{"description":"Object-store key where the bytes live (e.g. tattoos/{id}.png); omitted when empty.","type":"string"},"image_url":{"description":"Viewer URL for the stored image (/api/v1/tattoos/{id}/image).","type":"string"},"ingested_at":{"description":"RFC3339 UTC timestamp the record was ingested; omitted when empty.","type":"string"},"labels":{"description":"The labels stored with the record (omitted if empty).","items":{"type":"string"},"type":"array"},"score":{"description":"Similarity score; always 0 on an ingest response (only meaningful on search hits).","type":"number"}},"required":["id","image_url","score"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Synchronous, back-compat indexing path that embeds the tattoo image, stores its bytes in the object store, and upserts the vector into the Qdrant index in-band, returning the created gallery record.","tags":["Tattoos"],"x-stability":"experimental"}},"/api/v1/tattoos/search":{"post":{"description":"Ranks the tenant gallery against a query by cosine nearest-neighbor over the pixelMapper vision-language embeddings, accepting EITHER a base64 query image (data) OR an existing tattoo id, and returns the top-k hits (id, image_url, image_key, labels, score) ordered by similarity, highest first. When both id and data are supplied, id takes precedence; supplying neither is a 400.","requestBody":{"content":{"application/json":{"example":{"id":"drozdik-v0-00000","k":3},"schema":{"properties":{"data":{"description":"Base64 image bytes or a data: URL to embed and search by; used only when id is empty.","type":"string"},"id":{"description":"Existing tattoo id to search by (searches by that record's stored vector); takes precedence over data when both are set.","type":"string"},"k":{"description":"Number of hits to return; defaults to 10 when omitted or \u003c=0.","type":"integer"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"hits":[{"id":"drozdik-v0-02028","image_key":"tattoos/drozdik-v0-02028.png","image_url":"/api/v1/tattoos/drozdik-v0-02028/image","labels":["source-hf-drozdik-v0","a","dragon","with","flames"],"score":0.9999999},{"id":"drozdik-v0-03296","image_key":"tattoos/drozdik-v0-03296.png","image_url":"/api/v1/tattoos/drozdik-v0-03296/image","labels":["source-hf-drozdik-v0","a","dragon","with","flames"],"score":0.99939406},{"id":"drozdik-v0-01413","image_key":"tattoos/drozdik-v0-01413.png","image_url":"/api/v1/tattoos/drozdik-v0-01413/image","labels":["source-hf-drozdik-v0","a","dragon","with","flames"],"score":0.99939406}]},"schema":{"properties":{"hits":{"description":"Ranked matches, highest score first; each is a Record with id, image_url, image_key, labels, and score (similarity in [0,1]).","items":{"type":"object"},"type":"array"}},"required":["hits"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Ranks the tenant gallery against a query by cosine nearest-neighbor over the pixelMapper vision-language embeddings, accepting EITHER a base64 query image (data) OR an existing tattoo id, and returns the top-k hits (id, image_url, image_key, labels, score) ordered by similarity, highest first.","tags":["Tattoos"]}},"/api/v1/tattoos/{id}/image":{"get":{"description":"Serves one tattoo's raw PNG bytes (image/png) for the viewer and grid thumbnails, streaming the object-store bytes directly rather than JSON. The id comes from the path segment, falling back to an ?id= query param if the path value is empty.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Tattoo id, taken from the {id} path segment (falls back to the ?id= query param if the path value is empty).","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"(binary)":{"description":"Raw PNG image bytes; Content-Type image/png, Cache-Control 'private, max-age=86400, immutable'. Not JSON.","type":"string"}},"required":["(binary)"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Serves one tattoo's raw PNG bytes (image/png) for the viewer and grid thumbnails, streaming the object-store bytes directly rather than JSON.","tags":["Tattoos"]}},"/api/v1/telemetry/logs":{"post":{"description":"OTLP/HTTP logs sink: accepts an OpenTelemetry ExportLogsServiceRequest (OTLP/JSON, gzip-optional) and writes one gold row per log record into the Iceberg table telemetry.otel.logs. Each row carries the event and observed timestamps, normalized severity_number and severity_text, the flattened body, trace_id/span_id (so a log line joins to the span and, via its trace_id, to metric exemplars), service_name, tenant_id, and a JSON attributes object. Also mounted at the standard OTLP path POST /v1/logs.","requestBody":{"content":{"application/json":{"example":{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"checkout-api"}},{"key":"tenant_id","value":{"stringValue":"acme"}}]},"scopeLogs":[{"logRecords":[{"attributes":[{"key":"order.id","value":{"intValue":"1234"}}],"body":{"stringValue":"order 1234 accepted"},"observedTimeUnixNano":"1700000000001000000","severityNumber":9,"severityText":"INFO","spanId":"eee19b7ec3c1b174","timeUnixNano":"1700000000000000000","traceId":"5b8efff798038103d269b633813fc60c"}]}]}]},"schema":{"properties":{"resourceLogs":{"description":"OTLP ResourceLogs array; each has resource.attributes[] and scopeLogs[].logRecords[]. Empty/absent yields zero rows and a 200 success.","items":{"type":"object"},"type":"array"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{},"schema":{"properties":{"(body)":{"description":"OTLP-standard ExportLogsServiceResponse. Empty object {} on full success. Status 200.","type":"object"}},"required":["(body)"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"OTLP/HTTP logs sink: accepts an OpenTelemetry ExportLogsServiceRequest (OTLP/JSON, gzip-optional) and writes one gold row per log record into the Iceberg table telemetry.otel.logs.","tags":["Telemetry"]}},"/api/v1/telemetry/metrics":{"post":{"description":"OTLP/HTTP metrics sink: accepts an OpenTelemetry ExportMetricsServiceRequest (OTLP/JSON, gzip-optional) and emits one gold row per numeric data point into the Iceberg table telemetry.otel.metrics. Gauge and Sum points contribute their point value; Histogram points contribute their count (full bucket layout is deferred to a future shredded child table). Each row carries metric name, type (gauge/sum/histogram), stringified value, timestamp, service_name, unit, description, exemplar_trace_id (to correlate a sample back to the producing span), tenant_id, and a JSON labels object of the data-point attributes. Also mounted at the standard OTLP path POST /v1/metrics.","requestBody":{"content":{"application/json":{"example":{"resourceMetrics":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"checkout-api"}},{"key":"tenant_id","value":{"stringValue":"acme"}}]},"scopeMetrics":[{"metrics":[{"description":"request latency","histogram":{"dataPoints":[{"attributes":[{"key":"route","value":{"stringValue":"/orders"}}],"count":"42","exemplars":[{"traceId":"5b8efff798038103d269b633813fc60c"}],"sum":1234.5,"timeUnixNano":"1700000000000000000"}]},"name":"http.server.request.duration","unit":"ms"},{"gauge":{"dataPoints":[{"asDouble":0.37,"attributes":[],"timeUnixNano":"1700000000000000000"}]},"name":"process.cpu.utilization","unit":"1"}]}]}]},"schema":{"properties":{"resourceMetrics":{"description":"OTLP ResourceMetrics array; each has resource.attributes[] and scopeMetrics[].metrics[], where each metric holds a gauge, sum, or histogram with dataPoints[]. Empty/absent yields zero rows and a 200 success.","items":{"type":"object"},"type":"array"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{},"schema":{"properties":{"(body)":{"description":"OTLP-standard ExportMetricsServiceResponse. Empty object {} on full success. Status 200.","type":"object"}},"required":["(body)"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"OTLP/HTTP metrics sink: accepts an OpenTelemetry ExportMetricsServiceRequest (OTLP/JSON, gzip-optional) and emits one gold row per numeric data point into the Iceberg table telemetry.otel.metrics.","tags":["Telemetry"]}},"/api/v1/telemetry/traces":{"post":{"description":"OTLP/HTTP trace sink: accepts an OpenTelemetry ExportTraceServiceRequest (OTLP/JSON, gzip-optional) as posted by a Collector's otlphttp exporter, converts every span into a flat gold row, and archives it in the Iceberg table telemetry.otel.traces correlated by trace_id/span_id. Each span becomes one row carrying trace/span/parent ids, name, normalized kind and status, service_name, start/end timestamps, duration_ns, scope name/version, tenant_id, and a sorted JSON attributes object; the owning tenant is resolved from the resource attribute tenant_id/tenant.id (falling back to the delivering identity's tenant). Also mounted at the standard OTLP path POST /v1/traces.","requestBody":{"content":{"application/json":{"example":{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"checkout-api"}},{"key":"tenant_id","value":{"stringValue":"acme"}}]},"scopeSpans":[{"scope":{"name":"otel-go","version":"1.28.0"},"spans":[{"attributes":[{"key":"http.status_code","value":{"intValue":"200"}}],"endTimeUnixNano":"1700000000123000000","kind":2,"name":"POST /orders","parentSpanId":"eee19b7ec3c1b173","spanId":"eee19b7ec3c1b174","startTimeUnixNano":"1700000000000000000","status":{"code":1,"message":""},"traceId":"5b8efff798038103d269b633813fc60c"}]}]}]},"schema":{"properties":{"resourceSpans":{"description":"OTLP ResourceSpans array; each has resource.attributes[] and scopeSpans[].spans[]. An empty/absent array yields zero rows and a 200 success.","items":{"type":"object"},"type":"array"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{},"schema":{"properties":{"(body)":{"description":"OTLP-standard ExportTraceServiceResponse. On full success this is an empty object {} (no partialSuccess field). Status 200.","type":"object"}},"required":["(body)"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"OTLP/HTTP trace sink: accepts an OpenTelemetry ExportTraceServiceRequest (OTLP/JSON, gzip-optional) as posted by a Collector's otlphttp exporter, converts every span into a flat gold row, and archives it in the Iceberg table telemetry.otel.traces correlated by trace_id/span_id.","tags":["Telemetry"]}},"/api/v1/tenant/admin/grants":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tenant"],"x-stability":"experimental"}},"/api/v1/tenant/admin/grants/revoke":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tenant"],"x-stability":"experimental"}},"/api/v1/tenant/admin/people":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tenant"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tenant"],"x-stability":"experimental"}},"/api/v1/tenant/admin/people/{user}":{"delete":{"parameters":[{"in":"path","name":"user","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tenant"],"x-stability":"experimental"}},"/api/v1/tenants":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tenants"],"x-stability":"experimental"}},"/api/v1/tenants/{tenant}/modules":{"get":{"description":"Returns the named tenant's enabled feature-module entitlements (currently just the proprietary pixelMapper Forensics/biometric suite). The frontend reads this to decide whether to render the Forensics navigation and pages at all. Resolution honors a stored per-tenant override in the object store and otherwise falls back to the fail-closed default (Forensics ON only for the 'demo' tenant, OFF for every other tenant).","parameters":[{"in":"path","name":"tenant","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"tenant":{"description":"Path segment: the tenant slug to read; when empty it falls back to the caller's own tenant (tenantOf).","type":"string"}},"required":["tenant"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"forensics":true},"schema":{"properties":{"forensics":{"description":"True if the tenant is entitled to use the proprietary pixelMapper biometric (tattoo + facial-match) module.","type":"boolean"}},"required":["forensics"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the named tenant's enabled feature-module entitlements (currently just the proprietary pixelMapper Forensics/biometric suite).","tags":["Tenants"]},"post":{"description":"Toggles (overwrites) the named tenant's module entitlements, persisting the posted forensics.Modules object to the tenant's object store at modules/settings.json. This is the module AUTHORING boundary: enabling the proprietary Forensics suite is a per-tenant licensing/need decision, so the route is restricted to platform administrators — a tenant cannot self-serve. On success it echoes the stored entitlements back.","parameters":[{"in":"path","name":"tenant","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"forensics":true},"schema":{"properties":{"forensics":{"description":"Whether to entitle the tenant to the pixelMapper biometric module; a missing key decodes to Go zero value false (disabled).","type":"boolean"},"tenant":{"description":"Path segment: the tenant slug whose entitlements are being set; a missing/blank slug returns 400.","type":"string"}},"required":["tenant"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"forensics":true},"schema":{"properties":{"forensics":{"description":"The persisted entitlement flag, echoed back from the stored object.","type":"boolean"}},"required":["forensics"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Toggles (overwrites) the named tenant's module entitlements, persisting the posted forensics.Modules object to the tenant's object store at modules/settings.json.","tags":["Tenants"]}},"/api/v1/tokens":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tokens"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tokens"],"x-stability":"experimental"}},"/api/v1/tokens/scopes":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tokens"],"x-stability":"experimental"}},"/api/v1/tokens/{id}":{"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tokens"],"x-stability":"experimental"}},"/api/v1/tokens/{id}/rotate":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Tokens"],"x-stability":"experimental"}},"/api/v1/transform-models":{"get":{"description":"Lists all transformation models in the caller's tenant, sorted alphabetically by name. Each model is a named SQL or Python unit with declared ref() dependencies on other models (dbt-style), stored as a tenant-scoped object at models/{name}.json.","responses":{"200":{"content":{"application/json":{"example":{"models":[]},"schema":{"properties":{"models":{"description":"Array of stored transformModel objects, sorted ascending by name; empty array when none exist.","items":{"type":"object"},"type":"array"},"models[].body":{"description":"SQL text or Python source of the model.","type":"string"},"models[].language":{"description":"Model language: 'sql' or 'python'.","type":"string"},"models[].name":{"description":"Model name (dot-joined segments of letters, digits, underscore, dash).","type":"string"},"models[].refs":{"description":"Names of upstream models this model depends on.","items":{"type":"string"},"type":"array"},"models[].updated_at":{"description":"RFC3339 UTC timestamp of the last write.","type":"string"},"models[].updated_by":{"description":"Subject that last wrote the model; omitted when empty.","type":"string"}},"required":["models","models[].name","models[].language","models[].body","models[].refs","models[].updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists all transformation models in the caller's tenant, sorted alphabetically by name.","tags":["Transform models"]}},"/api/v1/transform-models/build-order":{"get":{"description":"Computes the topological build order of all models in the tenant (dependencies before their dependents) by feeding each model's refs into a topological sort. Returns 409 if the dependency graph contains a cycle.","responses":{"200":{"content":{"application/json":{"example":{"build_order":[],"model_count":0},"schema":{"properties":{"build_order":{"description":"Model names in dependency order (deps before dependents); empty array when there are no models.","items":{"type":"string"},"type":"array"},"model_count":{"description":"Total number of models considered.","type":"integer"}},"required":["build_order","model_count"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Computes the topological build order of all models in the tenant (dependencies before their dependents) by feeding each model's refs into a topological sort.","tags":["Transform models"]}},"/api/v1/transform-models/slim-ci-plan":{"post":{"description":"Computes a dbt-Cloud-style Slim-CI build plan (#214): given the set of changed model names, it selects the changed models plus everything transitively downstream to build/test, deferring unchanged upstreams to prod (their outputs are read rather than rebuilt). Returns 409 if the selected subgraph contains a cycle.","requestBody":{"content":{"application/json":{"example":{"changed":["silver"]},"schema":{"properties":{"changed":{"description":"Names of the changed/modified models; must be non-empty. Unknown names still echo back as selected.","items":{"type":"string"},"type":"array"}},"required":["changed"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"build_order":["silver","gold"],"deferred":["bronze"],"selected":["gold","silver"]},"schema":{"properties":{"build_order":{"description":"Selected models in dependency order, considering only intra-selection edges (deferred upstreams are external).","items":{"type":"string"},"type":"array"},"deferred":{"description":"Models referenced by a selected model but not themselves selected — read from prod, not rebuilt. Sorted.","items":{"type":"string"},"type":"array"},"selected":{"description":"Set to build/test: changed models plus everything transitively downstream. Sorted.","items":{"type":"string"},"type":"array"}},"required":["selected","build_order","deferred"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Computes a dbt-Cloud-style Slim-CI build plan (#214): given the set of changed model names, it selects the changed models plus everything transitively downstream to build/test, deferring unchanged upstreams to prod (their outputs are read rather than rebuilt).","tags":["Transform models"]}},"/api/v1/transform-models/{name}":{"delete":{"description":"Deletes the named transformation model from the caller's tenant. Idempotent — deleting a non-existent model still succeeds. Returns 204 No Content with no body on success.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes the named transformation model from the caller's tenant.","tags":["Transform models"]},"put":{"description":"Creates or updates (upserts) a transformation model identified by the {name} path segment. Validates the name is key-safe, defaults/validates the language to sql or python, requires a non-empty body, and requires every ref to be a valid model name; on success it persists the model and echoes the stored object.","parameters":[{"in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"body":"select * from {{ ref('bronze') }}","language":"sql","refs":["bronze"]},"schema":{"properties":{"body":{"description":"SQL text or Python source; must be non-empty after trimming.","type":"string"},"language":{"description":"Model language; trimmed and lowercased. Empty defaults to 'sql'. Must be 'sql' or 'python'.","type":"string"},"refs":{"description":"Names of upstream models this model depends on; each must itself be a valid model name.","items":{"type":"string"},"type":"array"}},"required":["body"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"body":"select * from {{ ref('bronze') }}","language":"sql","name":"silver","refs":["bronze"],"updated_at":"2026-07-15T12:00:00Z","updated_by":"user:alice"},"schema":{"properties":{"body":{"description":"The stored body.","type":"string"},"language":{"description":"Resolved language ('sql' or 'python').","type":"string"},"name":{"description":"Model name from the path.","type":"string"},"refs":{"description":"The stored upstream refs.","items":{"type":"string"},"type":"array"},"updated_at":{"description":"RFC3339 UTC timestamp set at write time.","type":"string"},"updated_by":{"description":"Subject that performed this write (from the request subject); omitted if empty.","type":"string"}},"required":["name","language","body","refs","updated_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Creates or updates (upserts) a transformation model identified by the {name} path segment.","tags":["Transform models"]}},"/api/v1/uploads/multipart":{"delete":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Uploads"],"x-stability":"experimental"},"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Uploads"],"x-stability":"experimental"}},"/api/v1/uploads/multipart/complete":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Uploads"],"x-stability":"experimental"}},"/api/v1/uploads/multipart/parts":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Uploads"],"x-stability":"experimental"}},"/api/v1/uploads/multipart/stream":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Uploads"],"x-stability":"experimental"}},"/api/v1/usage":{"get":{"description":"Returns the calling tenant's metered activity from the operational usage tracker (the Usage.Rollup surface, distinct from the compute ledger): a flat list of every recorded UsageEvent plus a per-operation aggregate of event count, rows, and bytes. This is the lightweight billing read surface keyed on the tenant resolved from the request.","requestBody":{"content":{"application/json":{"schema":{"properties":{"(none)":{"description":"No body and no query params are decoded; the tenant is taken from request context (X-Tenant-Id / host / default).","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"by_op":{"compute.query":{"bytes":0,"events":461,"rows":6014},"event-ingest":{"bytes":4044876,"events":691,"rows":691},"ingest":{"bytes":344364,"events":37,"rows":6583}},"events":[{"At":"2026-07-15T23:12:17Z","Bytes":0,"Op":"compute.query","Resource":"main.sonar.documents","Rows":2,"Seconds":0,"Tenant":"demo"},{"At":"2026-07-15T23:05:38Z","Bytes":12103,"Op":"ingest","Resource":"main.sonar.documents","Rows":48,"Seconds":0,"Tenant":"demo"}],"tenant":"demo"},"schema":{"properties":{"by_op":{"description":"Aggregate keyed by operation name (e.g. compute.query, ingest, event-ingest); each value has events (int), rows (int64), bytes (int64).","type":"string"},"events":{"description":"Every raw UsageEvent for the tenant. NOTE: domain.UsageEvent has NO json tags, so keys are Go field names: At, Tenant, Op, Resource, Rows, Bytes, Seconds.","items":{"type":"object"},"type":"array"},"tenant":{"description":"The tenant ID the events belong to.","type":"string"}},"required":["tenant","by_op","events"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the calling tenant's metered activity from the operational usage tracker (the Usage.Rollup surface, distinct from the compute ledger): a flat list of every recorded UsageEvent plus a per-operation aggregate of event count, rows, and bytes.","tags":["Usage"]}},"/api/v1/usage/runs":{"get":{"description":"Lists recent metered compute runs from the append-only usage ledger (epic #190), newest-ended first, optionally filtered to a single warehouse and capped by a limit. Each run is an immutable RunUsage record priced once at record time via the rate card.","requestBody":{"content":{"application/json":{"schema":{"properties":{"limit":{"description":"Query param; max runs to return. Default 100. Only honored when parseable and 0 \u003c n \u003c 100 (a value of 100+ is ignored and the 100 cap applies).","type":"integer"},"warehouse":{"description":"Query param; if set, returns only runs for this warehouse ID.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"runs":[]},"schema":{"properties":{"runs":{"description":"Array of billing.RunUsage records: id, warehouse_id, size, kind (dag|pipeline|sql|query), run_ref (omitempty), duration_seconds, vfu, usd, started_at (RFC3339), ended_at (RFC3339).","items":{"type":"object"},"type":"array"}},"required":["runs"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists recent metered compute runs from the append-only usage ledger (epic #190), newest-ended first, optionally filtered to a single warehouse and capped by a limit.","tags":["Usage"]},"post":{"description":"Appends one metered compute run to the tenant's append-only usage ledger. The caller (the DAG/pipeline executor or SQL path) names the warehouse, run kind, and billable duration; the handler resolves the warehouse's size, prices the run into VFU and USD once via the default rate card, stamps started/ended timestamps, and persists the immutable record.","requestBody":{"content":{"application/json":{"example":{"duration_seconds":12.5,"kind":"sql","run_ref":"q-20260715-001","warehouse_id":"wh_demo"},"schema":{"properties":{"duration_seconds":{"description":"Billable window in seconds; must be positive (400 otherwise). Drives VFU/USD pricing and the started_at/ended_at stamps.","type":"number"},"kind":{"description":"Run kind; lowercased and must be one of dag, pipeline, sql, query (400 otherwise).","type":"string"},"run_ref":{"description":"Opaque reference to the originating run (e.g. DAG/pipeline/query id); trimmed and stored, omitted when empty.","type":"string"},"warehouse_id":{"description":"The warehouse the run executed on; must reference an existing warehouse (404 if unknown). Trimmed; empty is rejected 400.","type":"string"}},"required":["warehouse_id","kind","duration_seconds"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"duration_seconds":12.5,"ended_at":"2026-07-15T23:00:12Z","id":"a1b2c3d4","kind":"sql","run_ref":"q-20260715-001","size":"small","started_at":"2026-07-15T23:00:00Z","usd":0.68,"vfu":0.34,"warehouse_id":"wh_demo"},"schema":{"properties":{"duration_seconds":{"description":"The billed duration in seconds.","type":"number"},"ended_at":{"description":"RFC3339 end time (now, UTC).","type":"string"},"id":{"description":"Server-generated short-hex ledger record ID (reuses the warehouse-id generator).","type":"string"},"kind":{"description":"Normalized run kind.","type":"string"},"run_ref":{"description":"Echoed run reference; omitted when empty (json omitempty).","type":"string"},"size":{"description":"Warehouse size resolved from the warehouse record, used for pricing.","type":"string"},"started_at":{"description":"RFC3339 start time (now minus duration, UTC).","type":"string"},"usd":{"description":"USD cost computed from size and duration via the rate card.","type":"number"},"vfu":{"description":"VForce compute units computed from size and duration via the rate card.","type":"number"},"warehouse_id":{"description":"The resolved warehouse ID.","type":"string"}},"required":["id","warehouse_id","size","kind","duration_seconds","vfu","usd","started_at","ended_at"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Appends one metered compute run to the tenant's append-only usage ledger.","tags":["Usage"]}},"/api/v1/usage/summary":{"get":{"description":"Rolls the whole compute usage ledger up into grand totals plus per-warehouse and per-kind subtotals for the cost \u0026 usage dashboard (#179), and annotates whether the tenant's usage is actually billed based on its billing mode. Subtotals are sorted by USD descending (biggest cost driver first).","requestBody":{"content":{"application/json":{"schema":{"properties":{"warehouse":{"description":"Query param; if set, the rollup covers only runs for this warehouse ID.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"billed":true,"billing_mode":"hosted-usage","by_kind":[],"by_warehouse":[],"runs":0,"total_usd":0,"total_vfu":0},"schema":{"properties":{"billed":{"description":"Whether this usage is actually billed (true when billing_mode is hosted-usage; false for byoc-license tenants who see usage for visibility only).","type":"boolean"},"billing_mode":{"description":"The tenant's billing mode: hosted-usage or byoc-license.","type":"string"},"by_kind":{"description":"Subtotals per run kind: {key, runs, vfu, usd}, sorted by usd desc (tie broken by key asc).","items":{"type":"object"},"type":"array"},"by_warehouse":{"description":"Subtotals per warehouse: {key, runs, vfu, usd}, sorted by usd desc (tie broken by key asc).","items":{"type":"object"},"type":"array"},"runs":{"description":"Total number of runs in the rollup.","type":"integer"},"total_usd":{"description":"Sum of USD cost across all runs.","type":"number"},"total_vfu":{"description":"Sum of VFU (VForce compute units) across all runs.","type":"number"}},"required":["runs","total_vfu","total_usd","by_warehouse","by_kind","billed","billing_mode"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Rolls the whole compute usage ledger up into grand totals plus per-warehouse and per-kind subtotals for the cost \u0026 usage dashboard (#179), and annotates whether the tenant's usage is actually billed based on its billing mode.","tags":["Usage"]}},"/api/v1/verification":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Verification"],"x-stability":"experimental"}},"/api/v1/verification/audit-chain":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Verification"],"x-stability":"experimental"}},"/api/v1/verification/events":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Verification"],"x-stability":"experimental"}},"/api/v1/verification/lineage":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Verification"],"x-stability":"experimental"}},"/api/v1/verification/quality":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Verification"],"x-stability":"experimental"}},"/api/v1/verification/retention":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Verification"],"x-stability":"experimental"}},"/api/v1/verification/suppression":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Verification"],"x-stability":"experimental"}},"/api/v1/warehouses":{"get":{"description":"Lists the calling tenant's warehouses, newest first (sorted by created_at descending). Each entry is decorated with its size's per-hour VFU and USD rates so the UI picker needs no second call. Warehouses whose stored JSON fails to decode are silently skipped.","responses":{"200":{"content":{"application/json":{"example":{"warehouses":[]},"schema":{"properties":{"warehouses":{"description":"Tenant's warehouse views, newest first; empty array when none exist.","items":{"type":"object"},"type":"array"},"warehouses[].auto_suspend_secs":{"description":"Idle seconds before auto-suspend (0 = never).","type":"integer"},"warehouses[].created_at":{"description":"RFC3339 creation timestamp.","type":"string"},"warehouses[].created_by":{"description":"Subject that created the warehouse.","type":"string"},"warehouses[].id":{"description":"Warehouse id (16 hex chars).","type":"string"},"warehouses[].name":{"description":"Human-readable warehouse name.","type":"string"},"warehouses[].size":{"description":"Current T-shirt size (XS..XL).","type":"string"},"warehouses[].state":{"description":"running or suspended.","type":"string"},"warehouses[].updated_at":{"description":"RFC3339 last-update timestamp; also the auto-suspend idle anchor.","type":"string"},"warehouses[].usd_per_hour":{"description":"Size's dollars/hr from the default rate card.","type":"number"},"warehouses[].vfu_per_hour":{"description":"Size's VFU/hr accrual.","type":"number"}},"required":["warehouses","warehouses[].id","warehouses[].name","warehouses[].size","warehouses[].state","warehouses[].auto_suspend_secs","warehouses[].created_by","warehouses[].created_at","warehouses[].updated_at","warehouses[].vfu_per_hour","warehouses[].usd_per_hour"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Lists the calling tenant's warehouses, newest first (sorted by created_at descending).","tags":["Warehouses"]},"post":{"description":"Provisions a new warehouse for the calling tenant. The warehouse is always created in the suspended state (no compute runs until a job schedules onto it), gets a random 16-hex-char id, and stamps created_by/created_at/updated_at. Returns 201 with the decorated warehouse view.","requestBody":{"content":{"application/json":{"example":{"auto_suspend_secs":300,"name":"nightly-etl","size":"M"},"schema":{"properties":{"auto_suspend_secs":{"description":"Idle seconds before auto-suspend; negative values are clamped to 0 (never).","type":"integer"},"name":{"description":"Warehouse name; trimmed, must be non-empty (400 otherwise).","type":"string"},"size":{"description":"T-shirt size; case-insensitive, must be one of XS/S/M/L/XL (400 otherwise).","type":"string"}},"required":["name","size"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"auto_suspend_secs":300,"created_at":"2026-07-15T12:00:00Z","created_by":"user@example.com","id":"a1b2c3d4e5f60718","name":"nightly-etl","size":"M","state":"suspended","updated_at":"2026-07-15T12:00:00Z","usd_per_hour":0.4,"vfu_per_hour":4},"schema":{"properties":{"auto_suspend_secs":{"description":"Effective auto-suspend window.","type":"integer"},"created_at":{"description":"RFC3339 creation time.","type":"string"},"created_by":{"description":"Subject that created it.","type":"string"},"id":{"description":"Newly assigned warehouse id.","type":"string"},"name":{"description":"Warehouse name as stored.","type":"string"},"size":{"description":"Parsed/normalized size.","type":"string"},"state":{"description":"Always 'suspended' on create.","type":"string"},"updated_at":{"description":"RFC3339, equals created_at on create.","type":"string"},"usd_per_hour":{"description":"Size's dollars/hr.","type":"number"},"vfu_per_hour":{"description":"Size's VFU/hr.","type":"number"}},"required":["id","name","size","state","auto_suspend_secs","created_by","created_at","updated_at","vfu_per_hour","usd_per_hour"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Provisions a new warehouse for the calling tenant.","tags":["Warehouses"]}},"/api/v1/warehouses/auto-suspend":{"post":{"description":"Runs one on-demand auto-suspend sweep over the calling tenant's warehouses: every running warehouse idle past its auto_suspend_secs window (measured from updated_at) is switched to suspended. Best-effort — per-object read/write failures are collected into errors and the sweep continues. Returns a summary of what was scanned and suspended. This is the admin/on-demand equivalent of the background ticker.","responses":{"200":{"content":{"application/json":{"example":{"scanned":3,"suspended":1,"warehouses":[{"id":"a1b2c3d4e5f60718","idle_for":"10m0s","name":"nightly-etl"}]},"schema":{"properties":{"errors":{"description":"Per-object failure messages (omitted when empty).","items":{"type":"string"},"type":"array"},"scanned":{"description":"Number of decodable warehouses evaluated.","type":"integer"},"suspended":{"description":"Number of warehouses this pass switched to suspended.","type":"integer"},"warehouses":{"description":"Per-warehouse records of what was suspended (omitted when empty).","items":{"type":"object"},"type":"array"},"warehouses[].id":{"description":"Suspended warehouse id.","type":"string"},"warehouses[].idle_for":{"description":"Human duration since last activity (e.g. '10m0s').","type":"string"},"warehouses[].name":{"description":"Suspended warehouse name.","type":"string"}},"required":["scanned","suspended"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Runs one on-demand auto-suspend sweep over the calling tenant's warehouses: every running warehouse idle past its auto_suspend_secs window (measured from updated_at) is switched to suspended.","tags":["Warehouses"],"x-stability":"experimental"}},"/api/v1/warehouses/sizes":{"get":{"description":"Returns the static catalog of offered warehouse T-shirt sizes (XS through XL), each with its worker-slot concurrency, VFU/hr accrual rate, and the computed dollars-per-hour derived from the default global rate card. Requires no object store — the catalog is compiled-in platform config.","responses":{"200":{"content":{"application/json":{"example":{"sizes":[{"size":"XS","slots":1,"usd_per_hour":0.1,"vfu_per_hour":1},{"size":"S","slots":2,"usd_per_hour":0.2,"vfu_per_hour":2},{"size":"M","slots":4,"usd_per_hour":0.4,"vfu_per_hour":4},{"size":"L","slots":8,"usd_per_hour":0.8,"vfu_per_hour":8},{"size":"XL","slots":16,"usd_per_hour":1.6,"vfu_per_hour":16}]},"schema":{"properties":{"sizes":{"description":"Ordered list of size specs, one per offered T-shirt size.","items":{"type":"object"},"type":"array"},"sizes[].size":{"description":"Size code: XS, S, M, L, or XL.","type":"string"},"sizes[].slots":{"description":"Worker/task-queue concurrency the executor grants this size (doubles each step).","type":"integer"},"sizes[].usd_per_hour":{"description":"vfu_per_hour multiplied by the rate card's usd_per_vfu (0.10).","type":"number"},"sizes[].vfu_per_hour":{"description":"Billable VForce Units accrued per active hour.","type":"number"}},"required":["sizes","sizes[].size","sizes[].slots","sizes[].vfu_per_hour","sizes[].usd_per_hour"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Returns the static catalog of offered warehouse T-shirt sizes (XS through XL), each with its worker-slot concurrency, VFU/hr accrual rate, and the computed dollars-per-hour derived from the default global rate card.","tags":["Warehouses"]}},"/api/v1/warehouses/{id}":{"delete":{"description":"Deletes a warehouse by id for the calling tenant. Idempotent — it issues an object-store delete on the tenant-scoped key without first checking existence, and returns 204 No Content on success with no body.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the warehouse id; 400 if blank.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Deletes a warehouse by id for the calling tenant.","tags":["Warehouses"]},"get":{"description":"Fetches a single warehouse by id for the calling tenant, decorated with its size's per-hour VFU and USD rates. Returns 404 if no object exists at the tenant-scoped key.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"id":{"description":"Path parameter: the warehouse id.","type":"string"}},"required":["id"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"auto_suspend_secs":300,"created_at":"2026-07-15T12:00:00Z","created_by":"user@example.com","id":"a1b2c3d4e5f60718","name":"nightly-etl","size":"M","state":"suspended","updated_at":"2026-07-15T12:00:00Z","usd_per_hour":0.4,"vfu_per_hour":4},"schema":{"properties":{"auto_suspend_secs":{"description":"Idle window in seconds (0 = never).","type":"integer"},"created_at":{"description":"RFC3339 creation time.","type":"string"},"created_by":{"description":"Creator subject.","type":"string"},"id":{"description":"Warehouse id.","type":"string"},"name":{"description":"Warehouse name.","type":"string"},"size":{"description":"Current size.","type":"string"},"state":{"description":"running or suspended.","type":"string"},"updated_at":{"description":"RFC3339 last-update time.","type":"string"},"usd_per_hour":{"description":"Size's dollars/hr.","type":"number"},"vfu_per_hour":{"description":"Size's VFU/hr.","type":"number"}},"required":["id","name","size","state","auto_suspend_secs","created_by","created_at","updated_at","vfu_per_hour","usd_per_hour"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Fetches a single warehouse by id for the calling tenant, decorated with its size's per-hour VFU and USD rates.","tags":["Warehouses"]}},"/api/v1/warehouses/{id}/auto-suspend":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Warehouses"],"x-stability":"experimental"}},"/api/v1/warehouses/{id}/resize":{"post":{"description":"Changes an existing warehouse's T-shirt size. Loads the warehouse, validates the new size, updates size and updated_at (which also resets the auto-suspend idle anchor), persists it, and returns the decorated view. Does not change running/suspended state.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"size":"L"},"schema":{"properties":{"id":{"description":"Path parameter: the warehouse id.","type":"string"},"size":{"description":"New T-shirt size; case-insensitive, must be XS/S/M/L/XL (400 otherwise).","type":"string"}},"required":["id","size"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"auto_suspend_secs":300,"created_at":"2026-07-15T12:00:00Z","created_by":"user@example.com","id":"a1b2c3d4e5f60718","name":"nightly-etl","size":"L","state":"suspended","updated_at":"2026-07-15T12:05:00Z","usd_per_hour":0.8,"vfu_per_hour":8},"schema":{"properties":{"auto_suspend_secs":{"description":"Idle window (unchanged).","type":"integer"},"created_at":{"description":"RFC3339 creation time.","type":"string"},"created_by":{"description":"Creator subject.","type":"string"},"id":{"description":"Warehouse id.","type":"string"},"name":{"description":"Warehouse name.","type":"string"},"size":{"description":"Updated size.","type":"string"},"state":{"description":"Unchanged running/suspended state.","type":"string"},"updated_at":{"description":"RFC3339, advanced to now.","type":"string"},"usd_per_hour":{"description":"New size's dollars/hr.","type":"number"},"vfu_per_hour":{"description":"New size's VFU/hr.","type":"number"}},"required":["id","name","size","state","auto_suspend_secs","created_by","created_at","updated_at","vfu_per_hour","usd_per_hour"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Changes an existing warehouse's T-shirt size.","tags":["Warehouses"]}},"/api/v1/warehouses/{id}/state":{"post":{"description":"Suspends or resumes a warehouse by setting its state to 'running' or 'suspended'. The state string is lowercased/trimmed and must be exactly one of those two values (400 otherwise). Updates updated_at (resetting the auto-suspend idle anchor on resume) and returns the decorated view.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"state":"running"},"schema":{"properties":{"id":{"description":"Path parameter: the warehouse id.","type":"string"},"state":{"description":"Target state; case-insensitive, must be 'running' or 'suspended'.","type":"string"}},"required":["id","state"],"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"auto_suspend_secs":300,"created_at":"2026-07-15T12:00:00Z","created_by":"user@example.com","id":"a1b2c3d4e5f60718","name":"nightly-etl","size":"M","state":"running","updated_at":"2026-07-15T12:10:00Z","usd_per_hour":0.4,"vfu_per_hour":4},"schema":{"properties":{"auto_suspend_secs":{"description":"Idle window (unchanged).","type":"integer"},"created_at":{"description":"RFC3339 creation time.","type":"string"},"created_by":{"description":"Creator subject.","type":"string"},"id":{"description":"Warehouse id.","type":"string"},"name":{"description":"Warehouse name.","type":"string"},"size":{"description":"Current size.","type":"string"},"state":{"description":"New state (running or suspended).","type":"string"},"updated_at":{"description":"RFC3339, advanced to now.","type":"string"},"usd_per_hour":{"description":"Size's dollars/hr.","type":"number"},"vfu_per_hour":{"description":"Size's VFU/hr.","type":"number"}},"required":["id","name","size","state","auto_suspend_secs","created_by","created_at","updated_at","vfu_per_hour","usd_per_hour"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Suspends or resumes a warehouse by setting its state to 'running' or 'suspended'.","tags":["Warehouses"]}},"/api/v1/work/mine":{"get":{"description":"The caller's personal HITL work inbox (#992): open work items from every source — document review, MTSS plans overdue for a progress note, referrals past their closed-loop window — that are on this person's plate. Fail-closed and scoped server-side: district tier (MTSS or referral estate authority) sees all open work; a teacher sees only work whose students fall on their OpenFGA-confirmed roster sections; a subject with neither authority nor a roster gets 403, never an empty 200. An item reaches you because it is assigned to you, or because it is unclaimed in a queue whose owning FGA group you belong to — membership read LIVE at request time, never from a mirror registry. total is the badge count.","responses":{"200":{"content":{"application/json":{"example":{"items":[{"enqueuedAt":"2026-07-26T04:00:00Z","queueId":"7740258cdd2d3263","reason":"progress overdue — no progress logged","source":"mtss","state":"pending","subject":"mtss:plan:mtss-3f2a91","title":"MTSS attendance plan needs a progress note"}],"mine":0,"total":1,"unclaimed":1},"schema":{"properties":{"assignee":{"description":"Current holder; omitted when unclaimed.","type":"string"},"dueAt":{"description":"SLA due time, RFC3339; omitted when unset.","type":"string"},"enqueuedAt":{"description":"Enqueue timestamp, RFC3339.","type":"string"},"items":{"description":"The work items, due-first (undated last), each carrying the fields below.","type":"string"},"mine":{"description":"How many are already claimed by the caller.","type":"integer"},"queueId":{"description":"Owning queue id; omitted when empty.","type":"string"},"reason":{"description":"Why this needs a human; omitted when empty.","type":"string"},"source":{"description":"Work source: document | mtss | referral.","type":"string"},"state":{"description":"Lifecycle state: pending | in_progress.","type":"string"},"subject":{"description":"Typed work subject FQN (mtss:plan:\u003cid\u003e, referral:\u003cid\u003e) or, for document review, the page key.","type":"string"},"title":{"description":"Human-readable label for the inbox row; omitted when empty.","type":"string"},"total":{"description":"Number of items — the inbox badge count.","type":"integer"},"unclaimed":{"description":"How many are still waiting for someone to take them.","type":"integer"}},"required":["items","total","mine","unclaimed","subject","source","state"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"The caller's personal HITL work inbox (#992): open work items from every source — document review, MTSS plans overdue for a progress note, referrals past their closed-loop window — that are on this person's plate.","tags":["Work"]}},"/api/v1/work/{subject}/claim":{"post":{"description":"Claims a work item for the calling subject — the 'I'll take this' action. Idempotent for the same person; 409 Conflict when someone else already holds it. Entitlement is checked rather than inbox membership, so a colleague in the same queue racing for just-claimed work is told 'already claimed' instead of 'not yours'. Lands a work-claim record in the tamper-evident audit chain.","parameters":[{"in":"path","name":"subject","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"assignee":"counselor@example.com","queueId":"7740258cdd2d3263","reason":"progress overdue — no progress logged","source":"mtss","state":"in_progress","subject":"mtss:plan:mtss-3f2a91","title":"MTSS attendance plan needs a progress note"},"schema":{"properties":{"assignee":{"description":"Current holder; omitted when unclaimed.","type":"string"},"dueAt":{"description":"SLA due time, RFC3339; omitted when unset.","type":"string"},"enqueuedAt":{"description":"Enqueue timestamp, RFC3339.","type":"string"},"queueId":{"description":"Owning queue id; omitted when empty.","type":"string"},"reason":{"description":"Why this needs a human; omitted when empty.","type":"string"},"source":{"description":"Work source: document | mtss | referral.","type":"string"},"state":{"description":"Lifecycle state: pending | in_progress.","type":"string"},"subject":{"description":"Typed work subject FQN (mtss:plan:\u003cid\u003e, referral:\u003cid\u003e) or, for document review, the page key.","type":"string"},"title":{"description":"Human-readable label for the inbox row; omitted when empty.","type":"string"}},"required":["subject","source","state"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Claims a work item for the calling subject — the 'I'll take this' action.","tags":["Work"]}},"/api/v1/work/{subject}/complete":{"post":{"description":"Marks a work item done and clears it out of every inbox. The decision is recorded on the item and in the tamper-evident audit chain (work-complete), so who was told, when, and what they did all survive. Same entitlement check as claim.","parameters":[{"in":"path","name":"subject","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"decision":"progress logged","note":"met with student, aim line back on track"},"schema":{"properties":{"decision":{"description":"What was decided; defaults to \"completed\".","type":"string"},"note":{"description":"Free-text note recorded in the audit detail.","type":"string"}},"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"example":{"decision":"progress logged","state":"done","subject":"mtss:plan:mtss-3f2a91"},"schema":{"properties":{"decision":{"description":"The recorded decision.","type":"string"},"state":{"description":"Always \"done\".","type":"string"},"subject":{"description":"The completed work subject.","type":"string"}},"required":["subject","state","decision"],"type":"object"}}},"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Marks a work item done and clears it out of every inbox.","tags":["Work"]}},"/dapr/subscribe":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Platform"],"x-stability":"experimental"}},"/health":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"Liveness probe.","tags":["Platform"]}},"/metrics":{"get":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Platform"],"x-stability":"experimental"}},"/v1/logs":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Platform"],"x-stability":"experimental"}},"/v1/metrics":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Platform"],"x-stability":"experimental"}},"/v1/traces":{"post":{"responses":{"200":{"description":"OK"},"default":{"description":"Error (RFC-7807 problem+json)"}},"summary":"","tags":["Platform"],"x-stability":"experimental"}}},"servers":[{"url":"https://lakehouse.vforce360.ai"}]}
