Skip to content

The public verbs

GENERATED from the API's OpenAPI schema and the MCP tool registry. Do not hand-edit.

Every verb is one REST route under /v1/public/ and one MCP tool of the same name, mapped 1:1 by the MCP server — the tool is transport, not a second implementation, so a response over MCP is the response over REST. Each verb below carries its purpose (the route's own docstring), the MCP signature and docstring, the request fields and the response fields. The text beside each field is what that field licenses, verbatim from the model that produces it; — means the model carries no description for it. Models referenced by a field are described once under Schemas. Each verb also says WHO MAY CALL IT — the permission it asks for, the roles that hold it, and what a key confined to documents is answered — read from the access rules themselves. Concepts — an address, a receipt, a count, None versus Not recorded, contested, provenance, supersession — are in the pages beside this one; start at ../quickstart.md.

The verbs marked REST ONLY are the exception, each by design and with the reason under its heading: a REST route (and an SDK method) with no MCP tool.

MCP tool REST asks for purpose
limma_ask POST /v1/public/ask read Ask a natural-language question over the workspace substrate
limma_verify POST /v1/public/verify verify Verify a claim against the workspace's substrate (SLA: p95 < 500ms)
limma_region GET /v1/public/region any key Re-read a byte-exact region of a page — the receipt, checked by the caller
limma_documents GET /v1/public/documents any key List the workspace's documents — id, name, kind, pages, status, completeness — one page at a time
limma_derive POST /v1/public/derive verify Derive a goal attribute from the workspace's substrate
limma_generations GET /v1/public/generations any key The generation chain a document belongs to
limma_diff GET /v1/public/diff any key What changed in the address space between two generations of a document
limma_connect POST /v1/public/connect write Ingest a SQL table or a document-store export as addressed facts (structured source)
limma_correct POST /v1/public/correct write Retire a wrong relation row, recording who, when and why
limma_resolve POST /v1/public/resolve write Confirm or reject that two records are one thing (any entity type), and re-resolve
limma_corrections GET /v1/public/corrections any key Every correction recorded in a workspace
limma_precheck POST /v1/public/precheck read Check whether an action's preconditions are satisfied
limma_ingest POST /v1/public/ingest write Ingest content into a workspace (async; status returned)
limma_delete_document POST /v1/public/delete_document write Delete a synced document by its external id (provable erasure)
limma_reconcile GET /v1/public/reconcile write List the workspace's synced documents by external id (paged)
limma_apply_pack POST /v1/public/apply_pack manage_catalogue Apply a signed domain pack (relations, vocabulary, document kinds)
limma_attest POST /v1/public/attest verify Produce a JSON-LD certificate attesting the workspace's substrate state
limma_state GET /v1/public/state any key Return the workspace's Statement set with statuses
limma_facts POST /v1/public/facts read Return the RELEVANT FACTS for a question, without answering it
limma_search POST /v1/public/search read Search — a brief or constraints to the CLOSED SET of matching entities, with counts, receipts, None and Not recorded
limma_plan POST /v1/public/plan read Compile a question WITHOUT executing it — a dry run
none — REST only POST /v1/public/upload an upload link Upload ONE file through an upload link (REST — the link comes from limma_ingest with upload_link: true)
none — REST only POST /v1/public/upload_export write Upload an export file for connect (multipart; REST and the SDK only — the agent connects it by export_id)
none — REST only GET /v1/public/usage any key What this key holds and may do: documents by status, failures, facts, records, last activity, its role, scope and rate limit (REST only)

limma_ask

POST /v1/public/ask — Ask a natural-language question over the workspace substrate

Who may call it. Asks for read — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — narrows its evidence to the key's documents; warrant.coverage.sources_unreadable names a document that was withheld from the answer.

Purpose

Ask the workspace a question through the answer machine — the same one POST /api/workspaces/{id}/answer runs: the question is compiled to a plan, the plan is executed over the workspace's facts, and the answer comes back with its tiered warrant. The MCP tool limma_ask calls this verb.

scope (entity/period/attribute) is BEST-EFFORT: it is prefixed to the question as a disambiguation hint ("For , : ..."), not applied as a hard filter. An answer binds ONE source per workspace (a single-document answer).

Over MCP

limma_ask(question: str, workspace_id: str = '', scope: Optional[Dict[str, str]] = None, output_mode: str = 'full', require_tier: Optional[str] = None, depth: str = 'THOROUGH', facts: int = 100) -> Dict[str, Any]

Ask a natural-language question over a workspace's VERIFIED substrate.

facts — how many RELEVANT FACTS to return in warrant.facts alongside the answer: not just the value the answer used, but every fact retrieval found relevant, each with its coordinates (entity / attribute / period) and byte-addressed provenance. Defaults to 100. Read warrant.coverage beside it — facts_dropped_by_cap > 0 means you are holding a ranked PAGE of the relevant set, not all of it. It counts against every fact retrieval SCORED; facts_dropped_by_layer_cap is the part of that drop made INSIDE retrieval's per-layer caps, which raising facts cannot recover. Pass 0 for the answer alone.

depth — THOROUGH (default) runs a cold re-extraction synchronously; FAST defers it to the background heal daemon and returns immediately, with warrant.pending_deepening flagging the deferral so a later re-ask may warm-hit.

Returns the answer plus a warrant whose verification_level is the TRUST CONTRACT — treat each tier differently:

  • certified_numeric / certified_verdict (deterministic: true): a reproducible function of the ingested data, deterministically gated. Safe to act on like a database read. certified_form (when present) is the value rendered with its byte-earned unit scale — display that form, not the bare token.
  • corroborated: cross-checked by independent derivation paths but NOT deterministic and NOT gated.
  • sourced: grounded in cited cells but LLM-composed — it MAY BE WRONG BY DESIGN. Before acting on a sourced figure, verify it (limma_verify with the claim, or inspect structured_citations); do not present it downstream as certain. warrant.calibrated_error_rate (when present) gives this tier's MEASURED wrong-rate + confidence interval + provenance, so you can PRICE the risk: act on a sourced answer directly for a low-stakes decision, verify it for a high-stakes one. The rate is scoped to the dataset it was measured on (see its measured_on block) — it is not a universal guarantee.
  • llm_reasoned: contains ungrounded inference — lowest trust.
  • refused: no grounded answer exists; refusal_reason + refusal_missing_keys name exactly what to ingest or supply, then retry. NEVER fabricate the answer a warrant declined to give. READ refusal_reason BEFORE ACTING ON THE EMPTY KEY LIST. compile_failed means the engine NEVER REACHED YOUR DOCUMENTS — no plan was compiled, so nothing was looked up and nothing is being said about what the corpus contains. warrant.compile_error carries the compiler's own diagnosis. Do NOT read it as "the fact is missing" and do NOT ingest more: fix the deployment (or rephrase) and retry. insufficient_evidence is the opposite — the engine DID look and the fact was not there.

warrant.disambiguation (when present) means the substrate held COMPETING readings for the asked coordinate that it could not reconcile — a list of {concept, period, readings:[{value, source_id, table_index, row_index}]}. Do NOT silently act on one side: narrow with scope and re-ask, or verify which reading you mean.

warrant.coordinates tells you WHICH PARTS OF YOUR QUESTION addressed real structure: {required, bound, fully_bound, coordinates[], unbound[]}. unbound[] names each coordinate that did NOT bind BY ADDRESS — {concept, period, op, status, code} — where status is unbound (tried and holed) or unreached (an earlier hole stopped execution first). That is the difference between "I found the FY2022 figure you asked for" and "I found something".

READ IT AS PROVENANCE, NOT AS A DEFECT REPORT. An unbound coordinate means that part of the answer did NOT come from a structural bind — it does NOT mean the answer is missing or wrong: another answer lane can answer where the structural bind did not. So supply the named operand, narrow scope to the document that has it, or split the question when the warrant is REFUSED, or when your action specifically needs the structural bind — NOT merely because unbound[] is non-empty. fully_bound is a STRUCTURAL fact, not a correctness score — a coordinate can bind to the wrong row and still read as bound; use verification_level + calibrated_error_rate to price risk.

The field SURVIVES the require_tier withhold below: the value is withheld, the addresses still ship, because they carry no value.

Also returned: the grounding citations, a fingerprint, and structured_citations — source receipts, each carrying its OWN verdict. Branch on byte_verified: true means re-slicing that receipt's (page, char_start, char_end) was proved to reproduce the text, and false-or-absent means it was not — an offset recorded by the extractor is not the same claim as an offset proved against the document, and extractor-recorded offsets often start mid-word. Never quote a receipt as a document quotation without that flag.

output_mode trims response verbosity for token budget: 'full' (default), 'compact' (drops the per-segment breakdown), or 'raw' (answer + warrant only).

require_tier (tier enforcement) — for a CRITICAL action, set the minimum tier you will accept ('certified' | 'corroborated' | 'sourced'). If the answer's tier is below it, the server WITHHOLDS the value and returns a REFUSED warrant (reason=tier_below_required) naming the tier that was available. Use 'certified' before any irreversible/high-stakes action so the tool cannot even hand you a sub-bar answer to act on; omit it to receive whatever tier forms (labeled).

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • question (string, required) — —
  • scope (dict of string, nullable) — Optional filter scope. Subset of {entity, period, attribute}. When omitted, the question is asked over the full workspace.
  • output_mode (one offull,compact,raw`, default'full'`) — Response verbosity for token-budget control. 'full' (default) returns answer + per-segment breakdown + citations + warrant; 'compact' drops the per-segment breakdown; 'raw' additionally drops the top-level citations (they remain on the warrant), leaving the answer + warrant — the minimal machine payload.
  • require_tier (one ofcertified,corroborated,sourced, nullable) — Tier enforcement. For a critical action, set the MINIMUM trust tier you will accept. If the formed answer's tier is below it, the server WITHHOLDS the value and returns a REFUSED warrant (reason=tier_below_required) naming the tier that WAS available — so a badly-behaved agent is STOPPED, not merely warned. 'certified' = only act on a deterministically-gated answer; omit for the default (return whatever tier formed, labeled).
  • depth (string, default 'THOROUGH') — THOROUGH (default) runs a cold re-extraction synchronously; FAST defers it to the background heal daemon and returns immediately, with warrant.pending_deepening flagging the deferral.
  • facts (integer, default 100) — How many RELEVANT FACTS to return in warrant.facts — every fact retrieval found relevant, with its coordinates and byte-addressed provenance, not just the value the answer used. Defaults to 100; pass 0 for the answer alone. warrant.coverage.facts_dropped_by_cap tells you whether you hold a page or the whole set, counted against every fact retrieval SCORED — not against the pool that survived retrieval's own per-layer caps, which can never exceed 129 however large the workspace. warrant.coverage.facts_dropped_by_layer_cap splits out that first stage: a fact dropped there is unreachable at ANY facts value, while the rest is fixed by asking for more. Capped at 200.

Response

  • workspace_id (string, required) — —
  • question (string, required) — —
  • answer (string, required) — —
  • citations (list of string) — —
  • elapsed_ms (number, default 0.0) — —
  • segments (list of [PublicAskSegment](#publicasksegment)) — —
  • warrant ([Warrant](#warrant), nullable) — —

limma_verify

POST /v1/public/verify — Verify a claim against the workspace's substrate (SLA: p95 < 500ms)

Who may call it. Asks for verify — roles: admin, analyst, service. A key scoped to workspaces reaches only those. A key confined to documents may call it — verifies against the key's documents; sources_unreadable names the rest.

Purpose

Verify a single claim against the workspace catalogue and current substrate state.

The endpoint runs the full substrate-verification pipeline on the workspace's stored claims PLUS the supplied claim and supports (projected as additional Statements), and returns the contradictions the SUPPLIED VALUE causes — see the attribution block below. verified=True means the substrate detected no rule violation that implicates the claim; read evidence_considered to tell that apart from "there was nothing to check against".

SLA target: p95 < 500ms for workspaces under 10K claims. Attribution costs a second pipeline pass over the stored claims (skipped when the workspace is empty); the catalogue is loaded once and shared.

Over MCP

limma_verify(workspace_id: str = '', claim: Optional[Dict[str, Any]] = None, supports: Optional[List[Dict[str, Any]]] = None, checks: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]

Check a claim's VALUE, or recompute YOUR OWN REASONING, against the store.

TWO INDEPENDENT HALVES. Send either, or both.

claim + supports — {attribute, entity, period, value} specs run through the substrate's rule engine. Returns {verdict, contradictions, substrate_conflicts, evidence_considered}:

  • verdict — verified / refused / unverifiable, folded over BOTH halves of the request: refused if your value contradicts the store OR a check you sent ran and does not hold; verified if stored evidence was examined or at least one check recomputed and held. THE THIRD IS REAL: it means nothing in this workspace could bear on your claim AND no check could resolve its operands, so no confirmation was possible. It is not a soft no. verified (the boolean) is verdict == "verified".
  • contradictions — caused by YOUR value: it disagrees with every stored reading at its coordinate, or it broke a rule that held without it. These drive refused. Change the value and this list changes.
  • substrate_conflicts — the workspace's OWN disagreements on that attribute, present whatever you asked. They do NOT set the verdict, but a coordinate whose sources conflict is one where no assertion is safe.
  • evidence_considered — how many stored facts sat at the claim's coordinate.

checks — a list of things YOU did, recomputed against the stored bytes with no model on the path. Each returns its own three-valued verdict, in order:

  • {"kind": "derivation", "expression": "income / revenue", "claimed": 0.0214, "operands": [{"name": "income", "handle": "cell:<claim_id>"}, {"name": "revenue", "attribute": "revenue", "period": "FY2017"}]} — handles are the ids limma_facts gave you. A COORDINATE MATCHING MORE THAN ONE FACT IS unverifiable, never silently one of them: which revenue you meant is your question to answer, and choosing for you is how a stamped derivation ends up resting on a number nobody picked.
  • {"kind": "quote", "source_id": "...", "quote": "...", "page": 4, "span": [120, 180]} — byte-exact. A quote that matches only after normalisation comes back unverifiable, not verified.
  • {"kind": "existence", "coordinate": {"attribute": "revenue"}, "expect": "absent"} — an ABSENT expectation is confirmed only where the coordinate BOUND; an address nothing knows about says nothing about the documents.

Read units_checked on each derivation: false means the arithmetic ran but the operands carried no recorded units, so nothing dimensional was established.

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • claim ([ClaimSpec](#claimspec), nullable) — The claim to verify against the substrate's rules. Optional when checks is supplied — a caller asking only for deterministic recomputation has no stated value for the rule engine to run on.
  • supports (list of [ClaimSpec](#claimspec)) — Optional supporting facts to verify against. When empty, the endpoint uses the workspace's existing substrate.
  • checks (list of [CheckSpec](#checkspec)) — Deterministic recomputations of the AGENT'S OWN reasoning — derivations, quotes, existence — each answered VERIFIED / REFUSED / UNVERIFIABLE with no model on the path. Independent of claim: both may be sent, and each check's verdict stands alone.

Response

  • workspace_id (string, required) — —
  • verified (boolean, required) — TRUE ONLY WHERE SOMETHING WAS ACTUALLY CHECKED. Read verdict for the honest three-valued answer; this boolean is verdict == 'verified', kept for clients that branch on a boolean. It is FALSE for an unverifiable claim, which is the cautious direction: an agent that branches on this stops trusting a claim nothing confirmed.
  • verdict (one ofverified,refused,unverifiable`, default'unverifiable') — THE VERB'S REAL ANSWER, three-valued, folded over BOTH halves of the request.refusedmeans the substrate CONTRADICTS the claim, or a check ran and does not hold.verifiedmeans something was examined and held: stored evidence at the claim's coordinate, or a check that recomputed within tolerance.unverifiablemeans NOTHING WAS CHECKED — no evidence at the coordinate and no check could resolve its operands. Branch on this, not onverified`: an unverifiable claim is not a confirmed one.
  • contradictions (list of [ContradictionEntry](#contradictionentry)) — Contradictions the SUPPLIED claim causes: value disagreements with the substrate at the claim's own coordinate, plus every finding that would not exist without the supplied claim. Drives verified.
  • substrate_conflicts (list of [ContradictionEntry](#contradictionentry)) — Pre-existing findings on the claim's attribute that are present with or without it — the workspace's sources disagreeing among themselves. NOT the supplied claim's contradictions, so they do not set verified; read them anyway, because a coordinate whose own sources conflict is one where no assertion is safe.
  • evidence_considered (integer, default 0) — How many stored facts sit at the claim's coordinate — what the verdict was actually checked against. ZERO means verified=true is VACUOUS: nothing in this workspace could have contradicted the claim. Absence is not confirmation.
  • sources_unreadable (list of string) — —
  • checks (list of [CheckOutcome](#checkoutcome)) — One outcome per submitted check, IN ORDER, so an agent that sent ten steps of a chain gets ten verdicts. Independent by construction: the one that could not be checked does not contaminate the nine that could, and no check affects verified/verdict above — that pair belongs to the claim.
  • elapsed_ms (number, default 0.0) — —
  • dag_fingerprint (string, nullable) — —

limma_region

GET /v1/public/region — Re-read a byte-exact region of a page — the receipt, checked by the caller

Who may call it. Asks for any key — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — refuses a document the key does not hold.

Purpose

Re-read the exact characters a receipt points at, so you can check a fact against the document yourself.

Every fact Limma delivers carries a receipt — a document, optionally a page, and a character span. Pass the receipt here and compare the returned text with the fact.

A WHOLE PAGE, OR THE WHOLE DOCUMENT: omit end (and start) and the frame is read from start in pieces of at most max_chars; end_of_frame says the piece reaches the end, next_start where the next piece starts. Every reply carries frame_length and page_count, so no span need be probed to learn them.

Two receipt kinds. page_span: pass page (0-based) and the offsets address that page's text. doc_span: omit page and the offsets address the document's extracted text end to end — the only frame a source with no stored page layer has (every .txt, and documents ingested before page texts were kept; page_count is then 0).

The slice is exact: text is frame[start:end] with no trimming or normalisation. A span past the end of the frame is a 422, never trimmed: it means the receipt was written against a generation of the document that no longer exists, and returning neighbouring text would let a stale receipt confirm the wrong text.

Errors: 422 for an inverted span (end < start), a page past the last page, or a span past the end of the frame; 404 for a source this workspace does not hold or your key may not read (the same answer, so the response never reveals which documents exist), for a doc_span on a source with no extracted text, and for a page_span on a source that stored no page texts (re-request without page); 503 when the document store is unavailable.

Over MCP

limma_region(source_id: str, start: int = 0, end: Optional[int] = None, workspace_id: str = '', page: Optional[int] = None, max_chars: Optional[int] = None) -> Dict[str, Any]

Re-read the EXACT text a receipt points at, or read a whole page or document.

A receipt is document, optional page, character span: pass page for a page_span; omit it for a doc_span (offsets over the document's text, page_count: 0 when it has no pages). text is frame[start:end] verbatim, no trimming. A span past the end is an ERROR, never trimmed: the receipt was written against a superseded generation (stale).

Omit end to read the WHOLE frame from start in pieces of max_chars (default 8,000): end_of_frame, else next_start to read on. Every reply carries frame_length and page_count.

Request

  • workspace_id (string, nullable, query) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • source_id (string, query, required) — The document to read.
  • start (integer, query, default 0) — First character offset (default 0).
  • end (integer, nullable, query) — One past the last character offset. OMIT it to read the WHOLE frame from start in pieces of at most max_chars: the reply says end_of_frame, or gives next_start for the next piece.
  • max_chars (integer, query, default 8000) — With no end: the most characters one piece holds (default 8000, at most 50000). Ignored when end is sent — a receipt's span is returned whole.
  • page (integer, nullable, query) — 0-based page index for a page_span receipt. OMIT for a doc_span receipt — the offsets are then read against the document's extracted text, which is the only frame a source with no persisted page layer has.

Response

A byte-true slice of a document, and enough to prove it is one.

  • workspace_id (string, required) — —
  • source_id (string, required) — —
  • frame (one ofpage,document`, required) — WHICH COORDINATE SYSTEM the span was read in — the receipt's own two kinds.pageslices one page (page_span);documentslices the extracted text end to end (doc_span`), which is the only frame a document with no page layer has.
  • page (integer, nullable) — The page read, or null for a document-frame span.
  • span (list of integer, required) — —
  • text (string, required) — frame[start:end] EXACTLY — no normalisation, no trimming, no ellipsis. What is in the store is what is returned.
  • frame_length (integer, required) — Characters in the frame, so a caller can bound its next span.
  • page_count (integer, required) — Pages this document persisted. 0 means no page layer, and every receipt against it must be a document-frame span.
  • end_of_frame (boolean, default False) — True when this slice reaches the end of the frame: nothing of this page (or, with no page, of the document) is left to read past span[1].
  • next_start (integer, nullable) — Where the next piece of a WHOLE read starts: send it as start (and no end) to read on. Null when end_of_frame is true, and on a request that named its own end.

limma_documents

GET /v1/public/documents — List the workspace's documents — id, name, kind, pages, status, completeness — one page at a time

Who may call it. Asks for any key — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — lists the documents the key holds, and counts only them.

Purpose

THE WORKSPACE'S DOCUMENTS, so an agent never rebuilds source ids from scattered replies: id, name, kind, pages, status, when it was ingested and HOW COMPLETELY it was read (reading) — a table's rows with the relation that holds them.

Paged (limit, after → next_after), oldest first; total counts every document the caller may read. A key confined to documents — or an end user's asserted principals — sees only the documents it may read, and total counts only those: nothing in the reply says whether others exist. Current versions only: a replaced generation is not listed. 422 for an after this verb did not issue.

Over MCP

limma_documents(workspace_id: str = '', limit: int = 20, after: str = '') -> Dict[str, Any]

List the documents you may read, oldest first: document_id (the source_id other tools take), name, file_type, document_kind, status, pages, frame_chars, ingested_at — and reading: whether each was read COMPLETELY (complete; incomplete_because says why not). A table (CSV / XLSX) lists its rows' relation and columns in reading.rows — enumerate them with limma_facts relation_query. Paged: limit (≤ 200), pass next_after as after; total counts them all.

Request

  • workspace_id (string, nullable, query) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • limit (integer, query, default 20) — Documents per page (at most 200).
  • after (string, nullable, query) — The next_after of the previous page; omit for the first.

Response

One page of the documents this key may read, with how completely each was read.

  • workspace_id (string, required) — —
  • documents (list of dict) — Oldest first. Each: {document_id, name, file_type, document_kind, status, ingested_at, file_size_bytes, pages, frame_chars, reading}. document_id is the source_id every other verb takes; frame_chars is the length of the text limma_region reads (null: none stored); pages the pages it persisted (0: one document frame, no page layer; null: not measured). reading is {complete, incomplete_because, status, frame_chars, pages, parse_errors, pages_not_read, extraction_quality, rows}: complete is true only when the document was read to the end, its text stored, the parser reported no problem, no page went unread, the fact extraction read every region and — for a table — every row became a row; whatever was not recorded is UNKNOWN and keeps it false, with the reason in words. rows (a CSV / TSV / XLSX document; null otherwise) is {tables_total, rows_read, rows_not_read, cells_not_stored, error, tables: [{relation, sheet, rows_read, rows_not_read, cells_not_stored, refused, columns_total, columns: [{slot, header, type}]}]} — enumerate a table's rows with limma_facts mode: enumerate, relation_query: {relation, where} (at most 5 tables and 25 columns listed per document; the totals count them all).
  • total (integer, required) — Documents this key may read in the workspace, current versions only — the denominator: total == every page's returned summed.
  • returned (integer, required) — Documents in this page.
  • next_after (string, nullable) — Pass it as after for the next page; null on the last page.

limma_derive

POST /v1/public/derive — Derive a goal attribute from the workspace's substrate

Who may call it. Asks for verify — roles: admin, analyst, service. A key scoped to workspaces reaches only those. A key confined to documents may call it — derives from the key's documents; sources_unreadable names the rest.

Purpose

Backward-chain a goal through the catalogue.

Returns the derived value when the workspace + catalogue can produce one. Returns open: [missing_inputs] when the goal cannot be derived because some catalogue-rule inputs are not in the workspace.

The derivation walks the catalogue's rules from the goal toward their inputs; supporting_statement_ids names the statements a derived value rests on.

Over MCP

limma_derive(goal: str, workspace_id: str = '', entity: str = '', period: str = '') -> Dict[str, Any]

Derive a goal attribute from the substrate.

Returns {derived, supporting_statement_ids} when derivable, or {open: [missing_inputs]} naming exactly what's still needed — so the agent knows what to fetch to unblock the derivation.

Binds only the ENTITY'S OWN facts. A figure the document states about another party ("Heinz net sales were $4,100 million" in Kraft Heinz's filing) is stored on the entity's address with a slots value naming that party; it is never derived as the entity's value. limma_facts delivers it, with the slot.

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • goal (string, required) — Canonical attribute name the caller wants derived.
  • entity (string, default '') — —
  • period (string, default '') — —

Response

  • workspace_id (string, required) — —
  • goal (string, required) — —
  • derived (number, nullable) — The derived value when the workspace can produce one. Null when the goal is open (see open field).
  • supporting_statement_ids (list of string) — —
  • open (list of string) — Canonical attribute names that block derivation. Empty when the goal is already derivable / present.
  • sources_unreadable (list of string) — —

limma_generations

GET /v1/public/generations — The generation chain a document belongs to

Who may call it. Asks for any key — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — the chains of the key's documents.

Purpose

Which versions of this document exist, and which one is live.

An agent holding a receipt holds whatever generation was current when the receipt was written — which may be anywhere in the chain, including the middle. This is how it finds out that the ground moved under an answer it already gave.

TWO ACCESS RULES, and they answer different questions:

  • THE REQUESTED SOURCE, if this key cannot read it, is a 404 — the same answer an unknown source gets, and the same answer /region and /diff give.
  • A DIFFERENT GENERATION IN THE CHAIN that this key cannot read is simply absent from the list: naming it would reveal documents outside the key's scope.

Over MCP

limma_generations(source_id: str, workspace_id: str = '') -> Dict[str, Any]

Which versions of this document exist, and which one is LIVE.

You hold whatever generation was current when your receipt was written — which may be anywhere in the chain, including the middle. This is how you find out the ground moved under an answer you already gave.

Returns {generations: [...]} oldest first, each with is_current, superseded_by and duplicate_of. The last two are different answers to "where did my fact go": REPLACED by a new version, or folded into a document already held because the bytes were identical.

Request

  • workspace_id (string, nullable, query) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • source_id (string, query, required) — Any generation in the chain.

Response

  • workspace_id (string, required) — —
  • source_id (string, required) — —
  • generations (list of [GenerationEntry](#generationentry)) — The chain this document belongs to, OLDEST FIRST. Empty when the source is unknown or this key may not read it.

limma_diff

GET /v1/public/diff — What changed in the address space between two generations of a document

Who may call it. Asks for any key — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — refuses a document the key does not hold.

Purpose

Does the answer I gave last week still hold?

When a document is replaced, the replaced generation and everything it put in the address space leave every enumeration at once — a superseded reading is never counted. This verb tells an agent what that changed: per coordinate, whether it was added, removed or changed between the two generations (counts also reports unchanged).

removed is the outcome to read first: a coordinate the newer generation no longer populates — the re-read lost a fact.

Both generations are read whether or not they are current (the older side is retired by definition).

Over MCP

limma_diff(from_source_id: str, to_source_id: str, workspace_id: str = '', limit: int = 500) -> Dict[str, Any]

What changed in the address space between two generations of a document.

THE VERB TO CALL BEFORE RE-USING AN OLD ANSWER. Returns {counts, changes, linked, truncated} where each change is added / removed / changed:

  • removed — THE ONE THAT MATTERS. A coordinate that STOPPED being populated; the re-read lost a fact you may have answered from.
  • changed — carries relative_move, because a rounding correction and a restatement are the same boolean and different facts.
  • counts.unchanged — the denominator. 4 changed of 12 is a re-parse; 4 of 4,000 is a correction.

linked: false means you diffed two UNRELATED documents — arithmetically fine, and it means something completely different. truncated says the list was cut; the counts never are.

Request

  • workspace_id (string, nullable, query) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • from_source_id (string, query, required) — The older generation.
  • to_source_id (string, query, required) — The newer generation.
  • limit (integer, query, default 500) — Bounds the change LIST. Never the counts.

Response

  • workspace_id (string, required) — —
  • from_source_id (string, required) — —
  • to_source_id (string, required) — —
  • linked (boolean, required) — True when the two sources are actually one generation chain. FALSE MEANS YOU DIFFED TWO UNRELATED DOCUMENTS — arithmetically fine, and it means something completely different.
  • counts (dict of integer) — added / removed / changed / unchanged. unchanged is the denominator that makes the rest mean something: 4 changed out of 12 is a re-parse, 4 out of 4,000 is a correction.
  • from_total (integer, default 0) — —
  • to_total (integer, default 0) — —
  • truncated (boolean, default False) — True when changes holds fewer entries than counts reports. The counts are always complete.
  • changes (list of [ChangeEntry](#changeentry)) — Ordered by consequence — removed, then changed (largest move first), then added — so a truncated list shows the worst of what happened rather than the alphabetically first.

limma_connect

POST /v1/public/connect — Ingest a SQL table or a document-store export as addressed facts (structured source)

Who may call it. Asks for write — roles: admin, analyst, service. A key scoped to workspaces reaches only those. A key confined to documents is refused: 403 document_scoped_key_refused.

Purpose

Connect a table. Its columns are DECLARED axes, so nothing is inferred.

Unlike /ingest, which runs extraction over document bytes (parsing, table detection, label and scale inference), connect maps the columns you name to axes and stops: the facts it reads from columns carry declared provenance, never inferred.

Facts arrive with row_pk receipts, so an agent can re-read one against the LIVE table. A row that has since changed re-reads to its new value, which is how a stale import becomes a visible finding rather than a silently wrong answer.

A DOCUMENT-STORE EXPORT is the other structured source (document_source: a mongoexport file, or a CSV / TSV file or one sheet of an XLSX workbook, read under a stated document_mapping). One document states several rows (an array of roles), so every relation the mapping names is read in one pass and written as one generation; a value that cannot be read is quarantined per value, and the entity it belonged to is never none for it. mode: propose reads a sample and proposes a mapping, and writes nothing.

Over MCP

limma_connect(workspace_id: str = '', database_url: str = '', table: str = '', mapping: Optional[Dict[str, Any]] = None, schema_name: str = '', replaces: str = '', relation_mapping: Optional[Dict[str, Any]] = None, document_source: Optional[Dict[str, Any]] = None, document_mapping: Optional[Dict[str, Any]] = None, mode: str = 'pull', sample_size: int = 500, sample_values: bool = False, allow_shrink: bool = False, document_schema: Optional[Dict[str, Any]] = None, draft_schema: bool = False) -> Dict[str, Any]

Ingest a SQL TABLE — or an EXPORT FILE (JSON, CSV, Excel) — as addressed facts.

AN EXPORT FILE on the API host — a mongoexport file (JSON lines or a JSON array), a CSV / TSV file, or one sheet of an XLSX workbook: pass document_source {"path": "/abs/experts.jsonl", "collection": "experts"} instead of database_url / table. A file NOT on the API host (a hosted deployment) is UPLOADED first by the integrator — POST /v1/public/upload_export (multipart) or the SDK's upload_export; no tool carries a file — and you pass {"export_id": "<the id it answered>", "collection": "experts"} instead of path: the same reader and everything below. A pull that writes deletes the upload (export.deleted); an unknown id is 404 export_not_found; its rows' receipts re-read row_unreadable. A CSV or a sheet is read as records — one row, one record, the header naming the fields — through everything below. Its format is format or the file's name (.csv, .tsv, .xlsx); a CSV is UTF-8, its cells are text and an EMPTY cell is a missing value; the delimiter (, ; tab) is stated or read from the header line; a workbook of several sheets needs sheet, and one that hides rows needs hidden_rows (read/skip). A column holding LISTS ("a; b") is split only where the mapping states separators — the proposal asks. proposal.source says how the file was read. First mode="propose": it samples the file and returns proposal.mapping with null wherever the data cannot decide (proposal.unresolved asks each question) and writes nothing. THE PROPOSAL IS A DRAFT: every relation entry it writes carries "confirmed": null and a pull refuses it — check each path against the data, read what the entry does NOT read (the not_read of its relations[<relation>].confirmed question: fields that hold dates or name a party), correct it, then set true. READ EVERY QUESTION, not only the nulls: a tie's write_as is what the mapping holds for each candidate; a null on a path (iterate: the relation's rows may be an array's elements — candidates names the arrays; a period key (period_from_path / period_to_path / period_text_path): a period path is chosen for you only where it is the ONE candidate and EVERY sampled value of it reads — one value that is no date, two paths named for one end, a timeline beside a start, or an end among keys that were not listed, and the key is null, with each candidate's counts; a slot: several paths, or one not every value of which reads) is yours to state from the data or to remove — never to guess; an entry holding NOTHING but "iterate": null stands for ANOTHER PLACE that may hold more of that relation's rows — a second array, the record's own fields beside an array, or a list of values the proposal does not read for you (one entry per place; the question's sources / entries say what each would be read from), or a place NO mapping can read (rows two arrays deep, or under keys that are ids) — state the entry, or remove it knowing what that leaves out; a relation read from several places is complete for a subject only where EVERY place gave a row or reads "none" by its entry's empty_means / absent_means; and a question may have no null of its own (relations[].iterate). Then send that mapping back, every null answered, as document_mapping::

{"key_path": "_id", "entity_path": "_id", "as_of_path": "updated_at",
 "relations": [{"relation": "held_role", "iterate": "experiences[]",
                "slot_paths": {"company": "company_name"},
                "period_from_path": "date_from", "period_to_path": "date_till",
                "period_end": "exclusive", "missing_end": "open_if_latest"}],
 "documents": [{"path": "bio", "kind": "profile"}]}

A value that cannot be read is QUARANTINED, never guessed (quarantined[]: key, path, reason, consequence), and its entity is then never none: it reads undetermined and no count over it is licensed. That includes an ELEMENT of a mapped array that holds values and none where the entries of ONE RELATION read (element_unread: a role under other keys, a note) — judged relation by relation, never read as "empty". If such elements are NOT rows of that relation, say so on its entry (unread_means: "nothing" — a proposal asks it, with the counts): a row written under other keys then reads as no row. A bare year is never turned into a month (the row is kept undated). A changed mapping over the same collection is refused unless replaces names the current generation; an export that lost many documents is refused unless allow_shrink.

ONE THING, ENTERED TWICE — for ANY entity type (people, companies, contracts, products). An identity key in the mapping states how the export's records are recognised as one thing — {name_kind: person|organisation|label, given_name_path?, family_name_path?, identifiers: [{path, kind, compare_as: email|url|domain|code|text, shared_value_is_evidence, differing_values_veto}], corroborate: [relation], exclusive: [[relation, slot]]} — and limma_search then counts merged entities of the type. kind is your own word for what the identifier is (registration_number, contract_number, gtin, email); name_kind says how names compare (organisation: "Solvane" and "Solvane K.K." may be one company, "Ferrum Systems AG" and "Ferrum Systems Ltd" are two; label: a product name or contract title, exact text only). Both judgements of an identifier are REQUIRED (is one value ever held by two things? does one thing ever hold two?): a wrong true merges two things. Records merge only on that evidence, never on a name; weaker pairs are proposals. mode="propose" offers a draft BESIDE the mapping (proposal.identity.block, every judgement and confirmed null — refused until you answer them; the proposed mapping itself merges nothing). The reply's merging says what was merged and proposed, per entity type.

NO PACK NEEDED. In a workspace with no pack the proposal also drafts the RELATIONS from the records (proposal.schema: the record's fields as the slots of one relation, each array one more; mapping already reads them). Each drafted relation carries "confirmed": null too: check its slot types (evidence) and its not_read, set true, and send it back as document_schema with the mapping — that pull declares it, then loads. A re-sync sends none. (With a pack applied the proposal maps onto the pack; draft_schema=True asks for the draft instead.)

ONE EXPORT IS PULLED BY ONE CALL AT A TIME (a table or a collection): a second call waits, and past the wait is refused 409 pull_in_flight — nothing was written; call again when the first has answered (an unchanged export is then deduplicated). 503 store_write_failed likewise wrote nothing: call again.

A SQL TABLE: pass EXACTLY ONE of mapping (numeric axes, below) or relation_mapping (relation rows). relation_mapping turns each export row into one typed RELATION row of a relation the workspace's pack declares::

{"relation": "held_role", "entity_column": "expert_id",
 "slot_columns": {"company": "company", "title": "title"},
 "key_columns": ["row_id"], "period_from_column": "start_month",
 "period_to_column": "end_month", "as_of": "2024-06",
 "coverage_columns": {"screening": "screened"}}

Every row lands with provenance_kind: declared, confidence: 1.0 and a {db_table, row_key, db_column} receipt PER SLOT — limma_facts re-reads that cell — and the row's subject and period cells — when it delivers the row (byte_verified; reason: row_changed when the table moved since, including a row re-keyed or re-assigned to someone else; row_unreadable when this API process did not run the connect, e.g. after a restart). Every entity pulled gets a coverage row for the relation, so its absence of rows reads none rather than not_recorded. The WHOLE export is validated before anything is written: a value that fails its slot type, a duplicate or NULL key, a period_to before period_from is a 422 naming the row_key and column, and nothing is written. A ROUTINE RE-SYNC needs no replaces: re-connecting the same table with the same relation_mapping supersedes the current generation of that export by itself (listed in superseded; rows the new pull no longer states leave every address), and an unchanged export dedupes. Name replaces (a CURRENT source of this workspace) to retire a DIFFERENT export — a mapping change or another table. The new generation is written first and the old one retired after. Read relations.rows_written / relations.coverage on the response.

mapping states which column supplies which axis, and it is STATED rather than guessed on purpose: a heuristic deciding that rev_amt means revenue would be a guess wearing the authority of a declaration.

{"entity_column": "company", "period_column": "fiscal_period",
 "value_columns": ["revenue", "cogs"], "key_columns": ["row_id"],
 "units": {"revenue": "USD"}}

WIDE tables (each value column IS an attribute) and LONG tables (one column NAMES the attribute — set attribute_column) are both supported, and neither is safely detectable, which is why you say which one it is.

Because a schema DECLARES its axes, nothing is inferred here — no label inference, no scale detection — and the resulting facts carry declared axis provenance rather than inferred.

Re-connecting an UNCHANGED table writes nothing (deduplicated: true): the snapshot hash is the data's identity, so a nightly sync does not multiply your facts. On a mapping pull a corrected value updates the existing fact in place, so a citation you already hold still resolves, and a row the table dropped retires with the superseded generation; on a relation_mapping pull a changed row is a new row (a row's identity is its content) and the old one retires with the superseded generation.

key_columns is what makes the facts RE-READABLE. Without it they carry no receipt, which is reported in read_errors rather than silently accepted. Read skipped_rows and read_errors on every call: a pull that imported 4 rows of a 40,000-row table must not look like a pull that imported the table.

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • database_url (string, default '') — SQLAlchemy URL for the source database. A network URL's HOST must appear in this deployment's connectable-database allowlist (EPISTEM_CONNECT_ALLOWED_HOSTS); with none configured, network sources are refused, as is a network URL carrying query parameters. A file-backed URL (sqlite:////absolute/path.db) names a path on the API host rather than a database of yours, and is refused unless an operator lists its directory in EPISTEM_CONNECT_ALLOWED_SQLITE_DIRS (none by default); a workspace store is refused under any listing — this deployment's own file (by resolved path, so a symlink does not help) or any SQLite file holding the store's tables, which is what a copy or a backup is. READ-ONLY credentials are strongly advised: this connector issues SELECT and nothing else, but the account it uses is yours to scope.
  • table (string, default '') — Table or view. Must be a plain identifier. REQUIRED with database_url (a SQL source is given by a non-empty database_url; with a document_source leave both empty).
  • schema_name (string, default '') — Optional schema qualifier.
  • mapping ([ColumnMapping](#columnmapping), nullable) — The NUMERIC-AXIS mapping: columns become facts at (entity, attribute, period). Exactly one of mapping / relation_mapping is required.
  • relation_mapping ([RelationMappingIn](#relationmappingin), nullable) — The RELATION mapping: each export row becomes one typed relation row (held_role{company, title, ...} over a period) with a {db_table, row_key, db_column} receipt per slot, provenance_kind: declared, confidence: 1.0, and a coverage row for every entity pulled. The whole export is validated first: a row whose value fails its slot type, a duplicate or NULL key, a period_to before period_from REJECTS the pull (422, the row key and column named) and nothing is written. Exactly one of mapping / relation_mapping is required.
  • access (list of string, nullable) — WHO MAY SEE THE RECORDS of this source: principal strings ('user:alice@x.com', 'group:finance'), applied to EVERY record of the source (per-record access is not offered) — its facts, its earlier generations, and the text documents a document-store pull registers from it. Omitted, the list of the generation this pull supersedes carries over. [] names nobody. See docs/integrator/sync-and-access.md.
  • replaces (string, default '') — A prior source id this pull supersedes. A ROUTINE RE-SYNC NEEDS NONE: on a mapping pull a changed value updates its fact in place, and a relation_mapping pull supersedes every current generation of the SAME export (same database, table and relation mapping) by itself, as a mapping pull does for rows the table dropped — superseded names them — while an unchanged pull dedupes to the current generation. Name replaces to retire a DIFFERENT export: a re-connect after a mapping change, or another table. On a relation_mapping pull it must name a CURRENT source of this workspace (422 otherwise); the new generation is written first and the old one retired after, so rows the new pull restates stay current throughout and rows it drops leave every address.
  • document_source ([DocumentSourceIn](#documentsourcein), nullable) — An EXPORT FILE instead of a SQL table — a file on the API host (path) or one uploaded to this workspace (export_id): a document-store export (a mongoexport file), or a CSV / TSV file or one sheet of an XLSX workbook, whose rows are records keyed by the header — through the same proposal, mapping, generations and receipts (a row's receipt names its key and its column). Give exactly one source: this, or database_url + table. With mode: pull it needs document_mapping; every relation the mapping names is read in ONE pass and written as ONE generation. A value that cannot be read is QUARANTINED per value (quarantined[]: its key, path, reason and what it cost) — and the entity it belonged to is then never none and never absent: it reads undetermined (withheld_rows_unanswerable:<r>) for what its readable rows do not match, and no count over it is licensed complete. The same holds for an entity one place a relation is read from gave rows while ANOTHER place it is read from said nothing for it (an empty or missing array left at not_recorded): each place answers for itself.
  • document_mapping ([DocumentMappingIn](#documentmappingin), nullable) — The mapping for a document_source pull. Not with a SQL source. With mode: propose only separators may be sent (the columns you have said hold lists): the proposal is then made over the split records.
  • document_schema (dict, nullable) — The RELATIONS this pull declares for the workspace, from the collection's own records — NO PACK NEEDED. mode: propose drafts it (proposal.schema) in a workspace no pack declares a relation for (with a pack applied: on draft_schema): {relations: {<relation>: {confirmed, subject_kind, slots: {<slot>: <type> \| {type, axis, vocabulary, max_terms, terms}}}}} — the record's scalar fields are the slots of ONE relation, each array one relation more (a row an element); slot types are term, entity_ref, number, date, boolean. Every drafted relation carries "confirmed": null and a pull is REFUSED while one does: correct it, set true, send it back with the mapping. A term slot's vocabulary is observed (built by the pull from the values the whole export states there, refused past max_terms), core (the shared axis named by axis) or stated (your terms). Declared once: a re-sync sends none. A CHANGED schema over a current generation is refused (schema_changed) unless replaces names that generation, and a slot's type never changes under rows that carry it. Not with mode: propose, not with a SQL source.
  • mode (one ofpull,propose`, default'pull') —propose(document sources only) reads a deterministic sample and returnsproposal— a mapping with every question the data cannot answer leftnull, and, in a workspace with no pack, the relations themselves as a draft from the records (proposal.schema) — and WRITES NOTHING. Send it back, edited, asdocument_mapping(+document_schema`) to pull.
  • sample_size (integer, default 500) — propose only: documents sampled.
  • draft_schema (boolean, default False) — propose only: draft the relations from the records (proposal.schema) in a workspace that HAS a pack — the proposal is then made as for a workspace that declares none. Without it a workspace with a pack gets the mapping onto the pack's relations, exactly as ever (whatever of the export they read), and only a workspace no pack declares a relation for gets the draft.
  • sample_values (boolean, default False) — propose only: also return up to 10 sample values per field (fields[].samples) — ONLY values this workspace's own vocabulary resolves, on a field that vocabulary recognises (or the boolean words yes / no / true / false). Never a number or a date, never a field proposed as the key, the name, the witness or a document, never a value holding @, a link or more than four words; every other field says why it shows none (fields[].samples_withheld). Off by default. With it off no VALUE of the export is returned at all. Field NAMES are returned either way, and a key of the export is listed as one only when it reads as a field name, at least two sampled documents carry it and — inside an object or an array — the documents that carry the object bear it out (see proposal): never an address, a link, a number, a date or a name written in two words, nor a key of an object nobody shares a key of — but a one-word key that most of them share (the same few reviewers' surnames, usernames or staff ids on every record), and in an object that reads as a record a few more such keys, cannot be told from a field name, and are listed.
  • allow_shrink (boolean, default False) — Document pulls only: accept an export that lost more than max_shrink_rate of its documents (see there).

Response

  • workspace_id (string, required) — —
  • source_id (string, default '') — —
  • source_name (string, default '') — —
  • content_hash (string, default '') — Identity of the DATA. An unchanged re-pull has the same hash and writes nothing.
  • row_count (integer, default 0) — —
  • fact_count (integer, default 0) — —
  • claims_written (integer, default 0) — —
  • skipped_rows (integer, default 0) — Rows read that produced NO fact. The denominator: 4 of 12 is a broken mapping, 4 of 40,000 is dirty data.
  • deduplicated (boolean, default False) — True when this pull matched data already held and wrote nothing new.
  • read_errors (list of string) — What the connector could NOT read — truncation, absent columns, rows with no address. Empty means it read everything it named.
  • superseded (list of string) — Every generation this pull RETIRED, oldest first: replaces when named (on a relation_mapping pull), then every other current generation of the SAME export — the same database, table and mapping — which a routine re-sync supersedes by itself. Facts and rows the new pull no longer states leave every address with them — in the same commit as the new rows on a relation_mapping or document_source pull. Empty on a first pull and on a deduplicated one (it IS the current generation) — unless an earlier pull DIED after registering its generation: that never-written generation is retired by the next pull of its export and is named here.
  • relations (dict, nullable) — Present on a relation_mapping pull: {relation, table, rows_read, absence_rows, rows_written, statements, slot_entries, coverage, entities_covered, provenance_kind, confidence, replaced, rows_retired_from_replaced}. rows_written counts distinct relation rows (two export rows stating one fact are one row); coverage counts coverage rows written; rows_retired_from_replaced is how many rows of the superseded generations this pull did NOT restate and so retired. On a deduplicated pull no row is written (statements, slot_entries and coverage are 0) and rows_written counts the rows the current generation already holds for this export. Receipts are redeemed against the live table by the API process that ran the connect: another worker, or this one after a restart, reports every structured slot row_unreadable until the table is connected again there. On a document_source pull table is the collection label, relation is null when the mapping reads more than one relation, rows_read counts documents, and the block gains gaps: {withheld, silent, unattributed_documents} and by_relation: {<relation>: {elements_read, rows, entities_covered, entities_withheld, entities_silent, entities_place_silent (the relation is read from more than one place and one of them said nothing for an entity another gave rows — counted inentities_withheldtoo), empty_elements (elements that gave an entry nothing and hold nothing, or that another entry OF THE SAME RELATION over the array read, or that count as empty by yourunread_means: "nothing"— those last counted apart in elements_read_as_nothing), elements_unread (elements that HOLD something and that no entry of THIS relation over their array read — whichever other relation did: each is quarantinedelement_unreadand its entity withheld), undated: {reason: n}, slots_dropped: {slot: n}, witness_raised_to_start, open_not_latest, gaps}} (rows counts DISTINCT rows, as rows_written does: two identical elements of one record are one row). A withheld gap is an entity whose record states a row the loader could not read (never none for it); a silent one an entity the record says nothing about for the relation (known, and not_recorded); undated counts rows kept WITHOUT a period — a bare year is never turned into a month. stranded_retired: {generations, statements, coverage, gaps, rows} is present only when this pull found rows still current under an already-RETIRED generation of the line (a state an earlier release could leave behind, which no re-sync retired) and retired them: a count taken before this pull may have included them. A healthy line never shows it.
  • mode (string, default 'pull') — pull, or propose (nothing was written).
  • documents (dict, nullable) — Present on a document_source pull: per mapped text path, {read, present, registered, unchanged, repaired, replaced, retired, retired_by_reason, kept_unconfirmed, empty, not_registered, metadata_updated, queued, deferred, deferred_reason, deferred_by_reason}. Each registered text is its own document whose subject is the record's entity (limma_region reads it; a criterion search reads it), and it counts registered only once its text was read back as stored. deferred documents are stored with their text and were read by no model; an unchanged text is never registered or read twice. retired — a document registered earlier is no longer current, by what this pull knows of it (retired_by_reason): the pull met its record and the record names no usable subject any more (subject_removed — a call still exported with its expert removed is no longer hers; so is one the export now gives to somebody else whose new text could not be registered), or its text is gone, blank or not a string (text_removed — for a text mapped through an array (calls[].text): its element removed, or the array gone); the pull did not meet its RECORD at all (removed_upstream); or the path is mapped read: retire (retired_by_mapping). removed_upstream needs a pull that IDENTIFIED every document: when a line could not be read or a document has no usable key, the missing record may be that one, so its documents are counted kept_unconfirmed instead (a re-sync in which every document is identified retires what is really gone; a record the pull DID read is never held back so). Documents registered earlier under a path this mapping no longer names are never retired by it, and read_errors says how many are not maintained. A text the store cannot hold (U+0000, broken Unicode) is not_registered and quarantined text_unstorable. The documents register AFTER the rows are written: a failure there never fails the pull — it is said in read_errors, and sending the pull again registers the rest.
  • names (dict of integer, nullable) — Present on a document_source pull: what the pull did with the display names its records state (name_path) — {written, removed, unchanged}, counted per subject. A name is read for EVERY record that names its subject, whether or not the record holds a row or a text, and kept per subject under the generation: limma_search names a person by it where no registered document names them. It is no part of the data's identity, so a pull whose only change is a renamed person is deduplicated and still counts that name written; a subject whose records state two different names, or none, has no row. All zero under a mapping with no name_path; null when the store could not record them (said in read_errors — the rows are written).
  • merging (dict, nullable) — Present once an identity block was ever stated for the workspace: what RESOLVING WHICH RECORDS ARE ONE THING made of this pull, for EVERY entity type. status: complete; unchanged (nothing the resolver reads moved, so the earlier result stands); not_configured; config_conflict (every type's current collections record different blocks — nothing is merged); failed (with reason — the rows ARE written, and every reply counts one record as one entity until a run completes: send the pull again, or resolve). types: {<entity type>: {status, merged_entities, merged_records, merged_pairs_by_kind, proposals, proposals_by_kind, candidate_pairs, vetoed_pairs, hub_identifiers, common_fact_keys, name_status, identity_faults, merge_possible, seconds, config, config_changed_from?}} — a type's own status is config_conflict when ITS collections record different blocks. merge_possible: false means the block states no identifier that is evidence and no corroborate relation: only same-name proposals can be raised. ids_in_two_types counts entity ids two types' records both use — never merged.
  • identities (dict, nullable) — Under an identity block: what the pull did with who each record is — {written, removed, unchanged, faults: {reason: n}}, counted per subject. A fault (identifier_unreadable, identifier_not_text, name_too_long, name_conflict_in_source) never refuses a pull — no row depends on an identifier — and that value is simply never evidence. Null when the store could not record them (said in read_errors).
  • quarantined (list of dict) — The first 500 values a document_source pull set aside, sorted by key then path: {key, path, value, line, reason, consequence, relation, detail}. consequence says what it cost: document_skipped, row_withheld (the entity keeps its readable rows and is undetermined for the rest), slot_dropped, period_dropped (the row is kept undated), document_not_registered. value is cut to 200 characters; a non-scalar shows its type, and detail (the reader's own words) never shows more of the value than value does. A string the store cannot hold is set aside like any other unreadable value, the same on SQLite and Postgres: one holding U+0000 or broken Unicode (value_unstorable at a slot, text_unstorable in a document's text), a slot value over 2,000 bytes of UTF-8 (value_too_long), and a key or subject id that is either (key_unreadable / entity_unreadable: the document is skipped).
  • quarantined_total (integer, default 0) — —
  • quarantined_by_reason (dict of integer) — —
  • quarantined_by_consequence (dict of integer) — —
  • schema_declared (dict, nullable) — Present on a document_source pull that read relations declared from the collection's own records (document_schema): {origin, declared, relations, vocabulary} — origin is the reserved pack they are filed under (limma_connected, never a signed bundle's), declared the relations THIS pull declared, relations every such relation it read, vocabulary the term count of each axis it wrote (an observed axis holds every value the export has stated at its slot). They are ordinary relations from here on: limma_search constraints and limma_facts relation_query name them.
  • access (dict, nullable) — Present when an access list applies to this source's records: {access, documents_covered, access_carried_over} — the principals who may see every record of the source (sent with this pull, or carried over from the generation it superseded), and how many generations and documents carry it. Per-record access is not offered.
  • export (dict, nullable) — Present when the document_source was an UPLOAD (export_id): {export_id, deleted}. deleted: true — this pull wrote its generation and the export is gone: connect it again only by uploading it again. false on a proposal (the export is kept for the pull), and on a pull whose delete failed (the backend's own rule ends it: the upload's expires_at).
  • proposal (dict, nullable) — Present with mode: propose: {sampled, scanned, sample_rule, format, fields, mapping, evidence, unresolved, unmapped, relations_without_candidates, current_mapping, conventions} — and schema in a workspace no pack declares a relation for (or on draft_schema): {origin, collection, relations, not_read, declared}, the relations DRAFTED from the records with the mapping entries that read them already in mapping. Each drafted relation is {confirmed: null, subject_kind, reads, slots, party, period, not_read, questions}; each slot {type, evidence} (a term: axis, vocabulary, max_terms too) — evidence counts the sampled values, their kinds, how many read as the type and how many do NOT (the pull sets those aside by name). No value of the export is in it: a term's vocabulary is built by the pull. schema.not_read lists every path turned into nothing, with why. mapping is a document_mapping with null wherever the data cannot decide (unresolved asks each question with the counts that help); raw values are not echoed (fields[].resolved_terms are the pack's own terms; fields[].samples is null unless sample_values, and then holds only values a vocabulary of this workspace resolves — fields[].samples_withheld names why a field shows none: not_requested, not_text, not_in_a_vocabulary, proposed_as_key_path / proposed_as_name_path / proposed_as_as_of_path / proposed_as_a_document). FIELD NAMES: a key of the export is listed as a path only on evidence that it is a field — it reads as an identifier (ASCII letters, digits, underscores; not digit-first; at most 64 characters) and at least two sampled documents carry it. Below the top level the documents in which its object holds a key decide the rest. The object is keyed by data — none of its keys is listed — when more than half of them carry a key that does not read so (a map keyed by e-mail address, by date, by a reviewer's full name), or, with more than twenty keys most of them in one document each, a key no other document carries. Otherwise the keys more than half of them carry are listed, and a key FEWER carry is listed only in a record with a few optional fields — the object has such shared keys, every key of it reads as a field name, and its other keys are no more than the shared ones (a role's end date, which only a closed interval has; a field written in two spellings). ONE stray key in an array of records does not make it a map: its fields are listed and the stray key is counted. Every key not listed is counted under the one segment * — <path>.*, or <path>.*[] where it holds an array (unmapped[].names_not_listed: the object's count, stated on each of the two, with why) — and nothing is proposed from a path through it: state such a path yourself if it is a field. WHAT THAT LEAVES UNDECIDED IS A null IN THE MAPPING, never an omission: a relation read from an array some of whose keys were not listed says how many (evidence.relations.<relation>.keys_not_listed) and, where it found a start date and no end, carries period_to_path: null — as does one whose end stands at a path only SOME of whose sampled values are dates (most: the path is proposed and the pull sets aside each value that is none; evidence.relations.<relation>.period has the counts); an ARRAY OF OBJECTS not all of whose keys were listed is named, for EACH relation that is not read from it (whichever other relation is), in evidence.relations.<relation>.arrays_not_read, asked about under relations[].iterate, and is iterate: null on every such relation proposed from the document's own fields — as is an array ranked second for that relation. A relation read from ONE array while another array of objects may hold more of its rows (ranked second for it, or not fully read) gets one MORE entry per such array, {relation, iterate: null, slot_paths: {}}: state that array and its paths there, or remove the entry. A tie's candidates are paths from the document's top; write_as gives each as a mapping writes it. conventions.field_names states the rule in every proposal, and its bound: a one-word key that most of an object's holders share reads as a field name and is listed as one, and so are a few more in an object that reads as a record.

limma_correct

POST /v1/public/correct — Retire a wrong relation row, recording who, when and why

Who may call it. Asks for write — roles: admin, analyst, service. A key scoped to workspaces reaches only those. A key confined to documents is refused: 403 document_scoped_key_refused.

Purpose

A CORRECTION: one relation row leaves is_current — at no address in any query from this moment — with retired_by / retired_at / retired_reason on the row and exactly ONE audit event (actor, when, row, reason).

RETIRED, NEVER DELETED: the row stays in the store so /corrections can list it and an audit can reach a fact a past answer cited. Its documents' statements and its entity's coverage stay — the source still says what it said and was still read; the correction records that the fact is wrong. A re-load of the same content does not bring it back.

404 for a row this workspace does not hold AND for one no document your key may read states (the same answer, so a scoped key cannot probe for rows). 409 when the row is already corrected — the existing correction is returned in detail and is not overwritten. 422 for a blank reason or actor. 403 for a key without write permission (a viewer).

actor is who the CALLER says is correcting; the event also records the id of the API key that authenticated the call (caller_key_id), so the audit trail names both the stated actor and the credential that acted — and files the event under the credential, never under the actor (GET /v1/audit/events lists it as actor).

Over MCP

limma_correct(row_id: str, reason: str, actor: str, workspace_id: str = '') -> Dict[str, Any]

Retire a WRONG relation row: it leaves every address — limma_facts, search, every count — from this call on.

row_id is the row_id of a relations.rows[] entry. reason and actor are required and recorded on the row (retired_by / retired_at / retired_reason) and in exactly ONE audit event; audit_event_id names it. The row is retired, never deleted, so GET /v1/public/corrections lists it; its documents' statements and its entity's coverage stay (the source still says it and was still read — the correction records that the fact is wrong), and a re-load of the same content does not bring it back. A second correction of the same row is refused (HTTP 409) and the first one stands — the error carries it under detail.correction. A row no document your key may read states is a 404, exactly like a row that does not exist. A read-only (viewer) key is refused (HTTP 403): a correction changes the store. The audit event records actor as you state it and, beside it, the id of the API key that made the call.

WHAT A CORRECTION DOES TO A COUNT. The corrected row's true value is unrecorded, so while a document in scope still states it limma_facts withholds the completeness licence on that relation (narrowed_by: corrected_rows), limma_search reads an unmatched entity holding it as undetermined (never none), and an entity whose every row was corrected is not a licensed none. A re-sync whose export no longer states the row lifts it.

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • row_id (string, required) — The relation row to retire — the row_id a relations.rows[] entry carries.
  • reason (string, required) — Why the row is wrong. Required; recorded on the row and in the audit event.
  • actor (string, required) — Who is correcting it. Required; recorded on the row (retired_by) and as the audit actor.

Response

  • workspace_id (string, required) — —
  • row_id (string, required) — —
  • retired (boolean, required) — True when THIS call retired the row. A row already corrected is a 409 carrying the existing correction, never overwritten.
  • correction ([CorrectionEntry](#correctionentry), required) — —
  • audit_event_id (string, nullable) — The id of the ONE audit event this correction emitted (actor, when, row, reason); null only when the deployment runs no audit emitter.

limma_resolve

POST /v1/public/resolve — Confirm or reject that two records are one thing (any entity type), and re-resolve

Who may call it. Asks for write — roles: admin, analyst, service. A key scoped to workspaces reaches only those. A key confined to documents is refused: 403 document_scoped_key_refused.

Purpose

An OPERATOR'S DECISION about which records of one entity type are one thing — and the re-resolve it causes, applied together as one unit.

The workspace merges two records only on evidence a different thing cannot plausibly share; everything weaker is a proposal that merges nothing. This is how a proposal is answered (same_entity) and how a merge is UNDONE (different_entities — the pair is split whatever the evidence, and is never proposed again). No row is touched by either: a merge is a registry applied when an answer is assembled. The latest decision on a pair wins, and each decision is ONE audit event (entity_link_decision: actor, reason, the pair, the type, what it replaced), filed under the key that authenticated the call.

422 for a pair of one record, a blank reason or actor, or a record of ANOTHER entity type than entity_type. 404 for a record this workspace does not hold AND for one your key may not read (the same answer, so a scoped key cannot probe). 409 type_not_configured when no identity block was stated for the type (a decision about it could merge nothing), and decision_conflict when the confirmations held and sent would connect a pair somebody rejected, or make one entity of more than 10 records — the conflict is named and NOTHING is written. 403 for a key without write permission.

With no decision it re-runs the resolver (every type): the recovery from a failed or stuck resolution (while one is, every reply counts one record as one entity).

subject: "names" answers the NAMING PROPOSALS instead — a label a similarity tier or a model read as an existing name. accept makes the label's name an ALIAS of that name: its facts are delivered at it and published under it, and no stored fact or receipt changes. reject closes the proposal; undo reopens it. With no name_decisions the reply lists the open proposals. A pair no document your key may read proposes is a 404. An accept whose two names hold DIFFERENT quantities at an entity and period both hold — different numbers (a segment's revenue and the company's), or a number in a unit the other never states there (a share in percent proposed as an amount) — is 409 values_disagree, naming the coordinates with both readings, their units and receipts — a power-of-ten twin among them is a scale misread, not a synonym — and nothing is written. Two spellings of an entity the workspace's entity registry joins ("PepsiCo", "PepsiCo, Inc.") are one entity there.

Over MCP

limma_resolve(entity_type: str = '', workspace_id: str = '', decisions: Optional[List[Dict[str, Any]]] = None, subject: str = 'entities', name_decisions: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]

Confirm or reject that two RECORDS are one THING (a person, a company, a contract, a product — any ENTITY TYPE) — and re-resolve the workspace. With subject="names": accept, reject or undo a NAMING PROPOSAL (below).

A workspace whose limma_connect stated an identity block for a type merges two records of it only on evidence a different thing cannot plausibly share, never on a name; everything weaker is a PROPOSAL (possible_duplicates on limma_search, entity_duplicate_proposals with kind: entity on limma_state) that merges nothing. This answers one, or undoes a merge.

entity_type: the type both records are (a search's merging.entity_type / counted.unit). decisions: [{entity_ids: [a, b], decision, reason, actor}] (at most 100) — decision is same_entity (one thing from now on, whatever their names or identifiers say) or different_entities (two: split whatever the evidence, and never proposed again). reason and actor are required. NO ROW IS TOUCHED by either: a merge is applied when an answer is assembled, so it is undone by the opposite decision — the latest on a pair wins. Each decision is one audit event.

Returns {entity_type, decisions: [{entity_ids, decision, previous, unchanged, audit_event_id}], merging: {status, types: {<type>: {merged_entities, proposals, …}}}, entities: [{entity_id, resolved_id, members, merged_by, possible_duplicates}]} — each named record as it is NOW.

Refused, and nothing written: HTTP 422 (one record twice, a blank reason or actor, a record of another type), 404 (a record the workspace does not hold, or one your key may not read), 409 type_not_configured (no identity block for the type) or decision_conflict (the confirmations would connect a pair somebody rejected, or make one thing of more than 10 records — detail.conflicting names it), 403 (a read-only key).

With no decisions it re-runs the resolver: the recovery when a reply says merging.status is failed or stuck pending (every reply then counts one record as one entity).

NAMES (subject="names"). A label no registered name equals keeps its own words; what a similarity tier or the model read it as is a PROPOSAL (limma_facts labels.proposals_open). name_decisions: [{name, candidate, decision, reason, actor}] (at most 100) — accept (name becomes an ALIAS of candidate: its facts are delivered at candidate and listed in its labels aliases; no stored fact changes; candidate must be a registered name), reject (closed, never counted open again) or undo (open again). Each change moves map_version and is one audit event. Returns {decisions: [{name, candidate, decision, previous, state, unchanged, audit_event_id, map_version}], names: {proposals, proposals_total, aliases, map_version}}; with no name_decisions it only lists the open proposals. Refused, and nothing written: 404 (a proposal no document your key may read makes), 409 candidate_not_registered / decision_conflict (a name made the alias of two names, or a cycle) / values_disagree (the two names hold different numbers, or numbers in different units, at an entity and period both hold — two quantities, never one name; detail.disagreements names each coordinate with both readings, their units and receipts; kind: unit marks another unit, kind: scale a power-of-ten twin, a scale misread; spellings the entity registry joins are one entity), 422 (a blank reason or actor).

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • subject (one ofentities,names`, default'entities') — WHAT the decisions are about.entities(the default): which RECORDS are one thing —entity_typeanddecisions.names: the naming PROPOSALS of the workspace's facts —name_decisions`; with none, the reply lists the open proposals to decide.
  • entity_type (string, nullable) — subject: entities only, and required there: the ENTITY TYPE the decisions are about — the subject kind of the records' relations (person, company, contract; a search's merging.entity_type). Both records of every decision must be of it, and it must have an identity block.
  • decisions (list of [EntityDecisionIn](#entitydecisionin)) — subject: entities. None: simply re-run the resolver — the recovery from a failed or stuck resolution.
  • name_decisions (list of [NameDecisionIn](#namedecisionin)) — subject: names: decisions on naming proposals, applied in order as ONE unit. None: the reply only lists the open proposals.

Response

  • workspace_id (string, required) — —
  • subject (one ofentities,names`, default'entities'`) — —
  • entity_type (string, nullable) — —
  • decisions (list of dict) — Per decision sent, in order. subject: entities: {entity_ids, decision, previous, unchanged, audit_event_id} — previous is the decision this one replaced on that pair ("" for the first). subject: names: {name, candidate, decision, previous, state, unchanged, audit_event_id, map_version} — previous / state the pair's state before and after (open / accepted / rejected), map_version the version the change made. unchanged — the pair already held this decision, and nothing was written.
  • names (dict, nullable) — subject: names only: {proposals, proposals_total, aliases, map_version} AS IT IS NOW, over the documents your key may read — proposals the open proposals (at most 100: {name, candidate, printed, tiers, documents, facts}, most documents first; tiers says which tier proposed it and its score), aliases the accepted decisions ({name, candidate, actor, decided_at, map_version}), map_version the map's version.
  • merging (dict, nullable) — The resolve this call ran — the merging block of a connect reply (status, and per entity type types.<type>: merged_entities, proposals, …).
  • entities (list of dict) — For every record a decision named: {entity_id, resolved_id, members, merged_by, possible_duplicates} AS IT IS NOW — resolved_id is the id the merged entity is listed under.

limma_corrections

GET /v1/public/corrections — Every correction recorded in a workspace

Who may call it. Asks for any key — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — corrections to rows the key's documents state.

Purpose

The corrections recorded in this workspace, oldest first — who retired which row, when and why. Scoped by your key's document ACL: a correction of a row no document you may read states is not listed.

Over MCP

limma_corrections(workspace_id: str = '') -> Dict[str, Any]

Every correction recorded in a workspace, oldest first — who retired which relation row, when and why (limma_correct writes them).

Returns {count, corrections: [{row_id, entity_id, relation, slots, period_from, period_to, retired_by, retired_at, retired_reason}]}. Scoped by your key's document ACL: a correction of a row no document you may read states is not listed. A corrected row is at no address in any other verb; this is where it is named. Erasing a document (that generation and every generation it replaced) erases the corrections of rows only they stated.

Request

  • workspace_id (string, nullable, query) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.

Response

  • workspace_id (string, required) — —
  • count (integer, default 0) — —
  • corrections (list of [CorrectionEntry](#correctionentry)) — Every correction in your scope, oldest first. A corrected row is at no address in any query (facts, search, the not-recorded listing is unaffected: its entity stays covered); it stays in the store so this list and the audit trail can name it.

limma_precheck

POST /v1/public/precheck — Check whether an action's preconditions are satisfied

Who may call it. Asks for read — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — decides on the evidence the key may read; sources_unreadable names the rest.

Purpose

Gate an action on the workspace's facts.

Each precondition is an attribute name. It is satisfied when a fact your key may read carries that attribute — compared by canonical name, so a catalogue synonym of a stored attribute counts. A figure recorded for another party (a fact with slots) does not satisfy it. Unsatisfied preconditions are listed in missing_evidence, and allowed is true only when none is missing; an empty preconditions list is allowed.

backward_chain_active says whether a precondition that is not present but is derivable from other facts through the catalogue counts as satisfied. It is false: only facts that are present satisfy a precondition.

Over MCP

limma_precheck(action: str, preconditions: List[str], workspace_id: str = '') -> Dict[str, Any]

Check whether an action's preconditions are satisfied by the substrate.

Returns {allowed: bool, missing_evidence: [...]} — a gate an agent calls before taking an irreversible action.

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • action (string, required) — Short identifier for the action being prechecked (e.g. 'send_loan_offer'). Returned verbatim in the response.
  • preconditions (list of string, required) — Canonical attribute names that must be present in the workspace before the action can proceed.

Response

  • workspace_id (string, required) — —
  • action (string, required) — —
  • allowed (boolean, required) — —
  • missing_evidence (list of string) — —
  • backward_chain_active (boolean, default False) — —
  • sources_unreadable (list of string) — —

limma_ingest

POST /v1/public/ingest — Ingest content into a workspace (async; status returned)

Who may call it. Asks for write — roles: admin, analyst, service. A key scoped to workspaces reaches only those. A key confined to documents is refused: 403 document_scoped_key_refused.

Purpose

Drop content into a workspace as a new source.

The content goes through the same ingestion pipeline as every other ingest door, /v1/ingest/webhook included.

A document may be as large as the deployment's upload limit (LIMMA_MAX_UPLOAD_BYTES, 100 MB unless the operator set another), measured in bytes of UTF-8 text; a larger one is refused 413. The request that carries it may be larger than the document — JSON escapes what it carries — up to twice that limit.

A FILE (a PDF, an Office file, an image) is not text: upload_link: true answers with a link for ONE upload of its bytes (POST /v1/public/upload) instead of reading content. wait_s (0–120) bounds how long the call waits for the document to be read, and wait says why it returned.

Over MCP

limma_ingest(content: str, workspace_id: str = '', filename: str = '', content_type: str = 'text/plain', idempotency_key: Optional[str] = None, document_kind: str = '', entity_name: Optional[str] = None, retry: bool = False, external_id: str = '', external_version: str = '', access: Optional[List[str]] = None, upload_link: bool = False, wait_s: Optional[float] = None) -> Dict[str, Any]

Ingest a document into a workspace. Returns {source_id, status, document_kind, document_kind_declared, document_kind_not_applied, entity_name, entity_name_not_applied, processing_status, retry_failed, ingest_reason, job_id, wait, upload_link}. ingest_reason says what this call decided: an extraction queued (no_job, row_failed, retry_requested, ...), absorbed by one already live (job_queued / job_running), or nothing to read (complete / partial).

A FILE (a PDF, an Office file, an image, a CSV / XLSX) is bytes, not content: send upload_link=True, its filename and content="". upload_link names ONE upload, valid 15 minutes: url, headers and command, a curl line reading filename from the working directory; run it where the file is, or hand it to your user. A key holder may also upload to POST /v1/workspaces/{id}/documents or run limma sync <folder>. A table (.csv / .tsv / .xlsx) is ALSO read as rows, one relation a table: limma_documents names it.

processing_status: complete (answerable); partial (read to the end, with defects; a re-send returns it as it is unless retry=True); failed (the same content re-sent under a new idempotency_key, or none, retries it once the failed attempt ended); pending / processing (queued or being read). The call waits up to wait_s (0-120 s; default 45). wait.outcome: read (wait.held counts its facts and records, wait.failure a failed one's reason), still_reading (re-send the same content later: it waits on the same document and never starts a second extraction), erased or no_reader (no worker alive; it stays queued).

retry=True reads a complete / partial document AGAIN (one extraction, replacing its pages_not_read) — e.g. a page refused model_unavailable once the provider is back. WRITE permission; counts against the ingest rate limit; queues nothing while an ingest of it is live; a failed retry leaves the document as it was, with retry_failed saying why. A file is retried by upload_link=True, retry=True.

entity_name is who the document is ABOUT — the subject of every row it yields (a profile's person, a filing's filer); without it the only name is guessed from the filename. idempotency_key: the same content and key return the original source_id. document_kind names what the document IS (profile for a person); empty is the workspace's default, and a kind it does not declare is a 422 naming those it does. Content already ingested keeps its first kind (document_kind_not_applied names this call's).

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • content (string, required) — Source content (text body). With upload_link: true it is the empty string: the file's bytes go to the link the reply names.
  • filename (string, default 'ingest.txt') — Filename to record on the source. Used for format detection: with upload_link it is REQUIRED, and its extension says how the uploaded bytes are read (report.pdf, deck.pptx).
  • upload_link (boolean, default False) — Answer with an UPLOAD LINK instead of reading content: a one-time, short-lived (15 minutes), size-capped capability for ONE file, bound to this workspace and to this key — upload_link: {method, url, headers, filename, max_bytes, expires_at, command}. POST the file's bytes to url with headers (the token travels in X-Limma-Upload-Token, never in the URL); command is that request as one curl line reading filename from the working directory. They are queued exactly as POST /v1/workspaces/{id}/documents queues a file, under this request's filename, entity_name, document_kind, retry, external_id, external_version and access, judged when they arrive by that door's rule for the key that minted the link. Send content empty and no idempotency_key.
  • wait_s (number, nullable) — How long to wait for the document to be read, 0 to 120 seconds; left out, the deployment's default (LIMMA_INGEST_WAIT_S, 45 s). The reply's wait says why the wait ended. With upload_link it is the UPLOAD's wait: the link's command carries it.
  • content_type (string, default 'text/plain') — MIME type hint. Influences how the ingest pipeline parses the content.
  • idempotency_key (string, nullable) — Optional client-supplied key for safe retries. Re-sending an ingest with the same (workspace_id, idempotency_key) returns the original source_id instead of creating a duplicate source. Best-effort, in-process (bounded LRU); a multi-worker deployment needs a shared store — see the deployment notes.
  • document_kind (string, default '') — The KIND of this document, which decides whose rows it can hold. Every row's subject is the uploaded document's own entity; a sentence never makes another party the subject. Under EVERY kind another party a sentence names is a 'company' / 'mentioned_entity' slot on the row, never an entity of its own. Built-in kinds: 'filing' (the filer's report — its segment table carries the filer's PARTS, addressed ' ()'; prose never mints a part, and a party it names — 'Canada' or 'Kraft Heinz Canada' — moves onto a part only when that document's table addresses it) and 'profile' (one declared entity, no sub-entities — send it for a document about a person); a pack applied to the workspace may declare more. EMPTY (the default) means the workspace's default kind: the default its applied pack declares, and 'filing' when no pack declared kinds (the finance catalogue is then the workspace's pack). An unknown kind, or an empty one in a workspace whose packs declare different defaults, is refused with 422 naming the kinds the workspace has. The kind recorded on the source is echoed in the response.
  • entity_name (string, nullable) — The entity this document is ABOUT: the subject of every row it yields (a profile's person, a filing's filer). Recorded as the source's registrant. Omitted, the subject is whatever the document itself states (a filing's cover page); when it states none, the ingest has NO declared subject — the name guessed from the filename is only a grouping key, and a party a sentence names keeps its own row instead of being filed under the guess. Send it for any profile, transcript or export row: without it 'every fact about ' cannot enumerate the document's rows.
  • retry (boolean, default False) — READ THIS DOCUMENT AGAIN. A plain re-send of content already read to the end (processing_status complete or partial) returns that document as it is and never reads it again, so a page it could not read stays in its pages_not_read. Send retry: true to queue ONE new extraction of it: the response reads pending (or the finished status when the extraction ends within the wait), and the extraction replaces the document's pages_not_read, extraction_health and ingestion_report — a page refused as model_unavailable (a provider outage) is read again. While an ingest of the document is already queued or running, a retry queues nothing and returns that ingest's status. A retry is never answered from the idempotency_key memo. It costs a whole extraction, so it needs a key with WRITE permission (a read-only key gets 403) and counts against this verb's rate limit; for a document not read to the end (pending, processing, failed) it changes nothing — a plain re-send already retries a failed one. The re-read keeps the document's recorded subject: the entity_name the source DECLARES is read again whether or not the retry names one, and a different name is not applied (see entity_name_not_applied). A retry whose extraction FAILS leaves the document as it was (complete or partial, its facts still delivered) and records the failure on retry_failed. This verb carries text; a PDF or an image is retried through the upload door, POST /v1/workspaces/{id}/documents with the form field retry=true.
  • external_id (string, nullable) — The SOURCE SYSTEM'S stable id for this file (a SharePoint item id, a Drive file id, an S3 key). A workspace never holds two live versions of one external_id: changed bytes are a NEW VERSION that replaces the old one — the old version is erased through the provable document delete, its facts stop answering, and sync.replaced names it with the rows read back (residue, {} is the proof). The same bytes again are a no-op that says so (sync.outcome: unchanged). The durable dedupe across processes; the idempotency_key memo is in-process only. See docs/integrator/sync-and-access.md.
  • external_version (string, nullable) — The source system's version of the file (an etag, a modified time). The same external_version again is a no-op answered before anything is read (sync.unchanged_because: same_external_version; sync.bytes_differ says when the bytes differed anyway). Needs external_id.
  • access (list of string, nullable) — WHO MAY SEE this document: principal strings ('user:alice@x.com', 'group:finance'). A platform key asserting X-Limma-Principals then reads it only for a principal the list names. Omitted: the document's list is left as it is (a new version keeps the old version's list). [] names nobody. Sent with an unchanged version it replaces the list without reading the document again.

Response

  • workspace_id (string, required) — —
  • source_id (string, required) — —
  • status (string, default 'queued') — —
  • idempotency_key (string, nullable) — —
  • document_kind (string, nullable) — The document kind this source's rows are read under: the kind RECORDED on the source (sources.document_kind) when the upload named one, else the workspace's default kind. Re-sending byte-identical content returns the existing source, read under the kind it was first ingested with — which may differ from this request's (see document_kind_not_applied).
  • document_kind_not_applied (string, nullable) — The kind THIS request named, when the source is read under a different one: the content was already ingested (a byte-identical upload returns the existing source and does not re-extract it), so the requested kind was NOT applied to its rows. None when the request named no kind or its kind is the one the rows are read under.
  • document_kind_declared (boolean, default False) — True when the upload NAMED its kind and the source records it. False means the source records no kind and is read under the workspace's default kind whenever it is extracted — so a pack applied later, whose default differs, changes how a re-extraction reads it.
  • entity_name (string, nullable) — The registrant RECORDED on the source (sources.entity_name): the entity the request declared, or the one the document stated. None means the ingest has no declared subject (IngestRequest.entity_name).
  • processing_status (string, nullable) — The source's lifecycle state READ FROM THE STORE after this request's ingest returned (sources.processing_status: pending / processing / complete / failed / partial), not a constant — unlike status, which is kept as it was for existing clients. complete means the document is answerable now. partial means it was read to the end and is answerable, with defects its ingest named and could not close (the source's ingestion_report lists them). failed means nothing was delivered (a failed RETRY of a complete or partial document does not make it failed: see retry_failed). pending / processing means it is queued for, or being read by, an ingestion worker and is not answerable yet: on a deployment with a durable queue the request waits for the document up to wait_s (else LIMMA_INGEST_WAIT_S) and returns before it finishes only when that wait ends or no worker is alive — wait.outcome says which; the document still completes. Re-send the same request (same content) to read the current status: a re-send never reads a complete or partial document again unless it carries retry: true, and retries a failed one once the attempt that failed has ended. None when no source row was produced. Read from the store on every response, a replay of an idempotency_key included.
  • entity_name_not_applied (string, nullable) — The entity_name THIS request named, when the source is not read under it: the content was already ingested and its source records a different entity (the declared one, or the one its document stated). A re-send — a retry included — never changes the subject a source records: a retry reads the document again under the entity it declares. None when the request named no entity or the source records that one.
  • retry_failed (dict, nullable) — The last RETRY of this document (retry: true on any ingest door, or the operator's re-extract) whose extraction failed: {error, failed_at, restored_to}. The document was complete or partial before that retry and is restored to that state (restored_to) — the facts it delivered are still delivered, and a plain re-send stays a status read. Anything the failed attempt wrote before it stopped is kept. None when no retry of it has failed since it was last read to the end.
  • ingest_reason (string, nullable) — What THIS request's ingest decided — the same rule on every ingest door. Queued an extraction: 'no_source_row' / 'no_job' (a new or unqueued document), 'row_failed' (a failed one retried), 'retry_requested' (retry: true of a complete or partial one), 'job_ended' / 'job_abandoned' (its last job ended without finishing it, or its worker died). Absorbed by an ingest already live — nothing queued, the response reports THAT ingest: 'job_queued', 'job_running', 'job_running_unmonitored'. Nothing to read: 'complete' / 'partial' (read to the end, no retry). Another door was deciding the same document at that instant: 'claim_contended', 'row_changed' — re-send to read the outcome. 'idempotency_replay': answered from the idempotency_key memo, which decided nothing (the row-derived fields are read from the store). None when the deployment runs no ingest queue.
  • job_id (string, nullable) — The queue job that owns this document's ingest (one job id per document): the job this request queued or the live one it was absorbed by. None when no job applies (a document read to the end, left as it is).
  • sync (dict, nullable) — Present when the request sent external_id or access: {external_id, external_version (as recorded), outcome: created | unchanged | replaced, unchanged_because: same_external_version | same_bytes, bytes_differ, replaced: {source_id, external_version, erased, generations_erased, residue, complete, jobs_forgotten, kept_because, interrupted (the erasure of the replaced version failed part way: what is left is in residue, and a document delete of its source_id finishes it)}, access (the document's list now; null = no list), access_updated, access_carried_over}.
  • wait (dict, nullable) — WHY THE CALL RETURNED WHEN IT DID: {outcome, wait_s (the bound), waited_s, held, reading, failure, erased, erased_at}. outcome is read (complete, partial or failed — held: {facts, records} counts what the document holds now, reading says whether it was read COMPLETELY — the inventory's reading (GET /v1/public/documents), a table's rows naming the relation they were written to — and a failed one carries failure: {reason}), still_reading (the bound passed while it is queued or being read — re-send to read the status again), erased (the document was deleted while the call waited; erased_at names when) or no_reader (no ingestion worker is alive: it stays queued until one starts). Null with upload_link, which reads nothing.
  • upload_link (dict, nullable) — Present when the request sent upload_link: true: {method, url, headers, filename, max_bytes, expires_at, uses, command}. Make the request it names, once, before expires_at, with the file's bytes as the body; its reply is the document door's entry for the file, and its wait. source_id is empty and status is awaiting_upload until then.
  • labelling (dict, nullable) — How far this document's PARTS are labelled for idea questions (limma_search with idea), read from the store: {state: not_started | queued | labelling | labelled | not_labelled, parts (null until counted), labelled, not_labelled: {cause: parts}, sentences_not_labelled} — cause model_unavailable (no model was configured: nothing was labelled, and an idea question counts these parts as not labelled), failed (a call failed; labelled again later), being_labelled; sentences_not_labelled counts the sentences of its labelled parts the model gave no usable topic, even when asked again. Labelling runs in the background after the document is read.

limma_delete_document

POST /v1/public/delete_document — Delete a synced document by its external id (provable erasure)

Who may call it. Asks for write — roles: admin, analyst, service. A key scoped to workspaces reaches only those. A key confined to documents is refused: 403 document_scoped_key_refused.

Purpose

Erase the live version of external_id through the provable document delete — every generation of it, every table that holds its data, its bytes — and read back what is left: residue is {} and complete true, or the answer is 500 erasure_incomplete naming the tables (the same call finishes it). A version that is also the live version of ANOTHER external id (identical bytes sent under two ids) is kept for that id (erased: false, kept_because); only this id's record and access grant go. 404 external_id_not_found for an id the workspace does not hold; 409 legal_hold when the document is held (nothing changed).

Over MCP

limma_delete_document(external_id: str, workspace_id: str = '') -> Dict[str, Any]

Erase the document an external_id names; residue ({} = none) is what is left.

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • external_id (string, required) — The source system's id the document was ingested under (external_id).

Response

  • workspace_id (string, required) — —
  • external_id (string, required) — —
  • source_id (string, required) — The live version the id held when asked.
  • external_version (string, nullable) — —
  • deleted (boolean, required) — The external id no longer names a document.
  • erased (boolean, required) — The document itself was erased. False when it is kept for another external id (kept_because), or was already gone (already_absent).
  • kept_because (string, nullable) — —
  • already_absent (boolean, nullable) — —
  • generations_erased (list of string) — —
  • claims_removed (integer, default 0) — —
  • relations_erased (dict of integer) — Rows removed per table, summed over the generations — and the trail: events_redacted / audit_events_redacted, the events that named the document, each kept as a shell (type, time, counts) with its content erased.
  • residue (dict of integer) — Rows still holding the erased generations, per table, read back from the database after the erasure — the event log and the audit trail included — {} is the proof.
  • jobs_forgotten (integer, nullable, default 0) — Ingest jobs of the erased generations deleted from the queue, with the document's bytes they held (a job still running takes its own record when it ends). null: the queue could not be asked — not complete; the same call asks again.
  • finished_interrupted_erasure (boolean, default False) — This call finished an erasure an earlier one began and a failure interrupted: the external id stays until the rest of the document is gone, so the same call finds it again.
  • complete (boolean, required) — The erasure left nothing (residue is {}, and the queue was asked).

limma_reconcile

GET /v1/public/reconcile — List the workspace's synced documents by external id (paged)

Who may call it. Asks for write — roles: admin, analyst, service. A key scoped to workspaces reaches only those. A key confined to documents is refused: 403 document_scoped_key_refused.

Purpose

Every external id the workspace holds, with its live version — {external_id, source_id, external_version, content_hash, recorded_at, processing_status, access} — in id order, limit a page; next_after pages on (null on the last page), total counts them all, access_required is the workspace's policy. A connector diffs this against its source system: an id the source no longer has is deleted (POST /v1/public/delete_document), a version that differs is sent again.

Over MCP

limma_reconcile(workspace_id: str = '', after: str = '', limit: int = 100) -> Dict[str, Any]

One page of the external ids the workspace holds, with versions; pass next_after as after.

Request

  • workspace_id (string, nullable, query) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • after (string, query, default '') — Exclusive: the next_after of the previous page (external ids in id order).
  • limit (integer, query, default 100) — —

Response

  • workspace_id (string, required) — —
  • documents (list of [ReconciledDocument](#reconcileddocument)) — —
  • total (integer, required) — Every external id the workspace holds.
  • next_after (string, nullable) — Pass as after for the next page; null on the last.
  • access_required (boolean, required) — The workspace's access policy.

limma_apply_pack

POST /v1/public/apply_pack — Apply a signed domain pack (relations, vocabulary, document kinds)

Who may call it. Asks for manage_catalogue — roles: admin, analyst. A key scoped to workspaces reaches only those. A key confined to documents is refused: 403 document_scoped_key_refused.

Purpose

A pack is a workspace's ADDRESS SPACE: the relations its facts are rows of, the vocabulary its term slots expand through ("DACH", "≥ VP", "net sales"), and the document kinds whose subject rule decides whose rows a document holds. Until a pack is applied a relation_query on its relations is relation_unknown and a relation_mapping has nothing to bind to.

The bundle is loaded from this deployment's registry and its signature VERIFIED before anything is written. Re-applying the same pack is idempotent (the refresh). After the apply the workspace holds EXACTLY what the applied version signs under its pack name: a relation, vocabulary or document kind an earlier version applied and this one does not carry is unloaded, and the response lists it (relations_unloaded, vocabulary_unloaded, document_kinds_unloaded, packs_released). 409 — nothing written — when the pack cannot coexist with what the workspace already has: its pack name is owned by a different bundle or signer, a relation it declares is declared by another pack, a slot it re-types is carried by current rows, a relation it stops declaring is carried by current rows, a document kind it declares is defined differently by another pack, or a document kind no pack would declare after it was recorded by current documents. 409 document_kind_default_unresolved — nothing written — when the apply would leave the workspace with no default document kind (send document_kind_default) or would change the default over documents that recorded no kind (send unnamed_sources); the detail carries the count, both defaults and the kinds. 403 for a key restricted to specific documents (a pack changes the address space of every document) and for a role without catalogue management. 404 for an unknown bundle id, naming the ids that exist.

Over MCP

limma_apply_pack(bundle_id: str, workspace_id: str = '', document_kind_default: str = '', unnamed_sources: str = '') -> Dict[str, Any]

Apply a signed domain PACK to a workspace — its ADDRESS SPACE.

A pack declares the relations a workspace's facts are rows of, the vocabulary their term slots expand through (synonyms, a hierarchy such as a region over countries, an order such as a seniority ladder), and the document kinds limma_ingest may name. Until a pack is applied, a limma_facts relation_query on its relations answers relation_unknown and limma_connect's relation_mapping has nothing to bind to.

The pack is loaded from the deployment's registry and its signature verified before anything is written; re-applying it is idempotent. Read relations_loaded, vocabulary_rows, document_kinds_loaded and document_kind_default (the kind an ingest naming no document_kind is read as). An unknown bundle_id is an HTTP 404 whose detail lists bundle_ids; a pack that cannot coexist with what the workspace already holds (its name owned by another bundle, a relation another pack declares, a re-typed slot rows depend on, a document kind defined differently) is a 409 and NOTHING is written.

A pack may DEPEND on others (the expert-network pack on the universal core, whose geography its slots read): they are applied first and listed in dependencies_applied with the relations each loaded — addressable here too.

A NEWER VERSION of a pack replaces the older one whole: what the older version applied and this one does not carry is unloaded and listed in relations_unloaded, vocabulary_unloaded, document_kinds_unloaded and packs_released. A relation current rows carry, or a document kind current documents recorded, is never unloaded — that apply is a 409 and nothing is written.

THE DEFAULT DOCUMENT KIND. A 409 with detail type document_kind_default_unresolved (nothing written) asks you to decide:

  • reason default_required: the workspace's packs would disagree on the default (a finance pack's filing, an expert pack's profile). Send document_kind_default — one of the detail's declared_kinds;
  • reason unnamed_sources_choice_required: the default would change and the workspace already holds unnamed_sources documents that recorded no kind. Send unnamed_sources="keep" (they stay read as document_kind_default_before) or "adopt" (they are read as the new default at their next extraction).

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • bundle_id (string, required) — The signed pack to apply, by bundle id. An id this deployment's registry does not ship is a 404 whose detail lists every id it does ship.
  • document_kind_default (string, nullable) — Pin the kind an ingest naming no document_kind is read as in this workspace. REQUIRED when the workspace's packs would otherwise disagree on the default (409 document_kind_default_unresolved, reason default_required); must be a kind the workspace declares after the apply.
  • unnamed_sources (one ofkeep,adopt, nullable) — What the workspace's current documents that recorded no kind ARE, when this apply changes the default kind. keep: record the kind they are read as now on each. adopt: read them as the new default at their next extraction. REQUIRED when the default changes and such documents exist (409, reason unnamed_sources_choice_required, with the count and both defaults).

Response

  • workspace_id (string, required) — —
  • bundle_id (string, required) — —
  • bundle_version (string, default '') — —
  • signer (string, default '') — The signer the pack verified against. With bundle_id it now OWNS pack in this workspace: another bundle re-using the pack name is refused (409).
  • pack (string, default '') — The pack name the relation sections apply under; empty when the bundle carries none.
  • relations_loaded (list of string) — Relations now addressable here: limma_facts relation_query and limma_connect relation_mapping bind to them.
  • vocabulary_rows (integer, nullable) — Vocabulary rows written (synonyms, hierarchy, order per axis); null when the pack carries no vocabulary.
  • document_kinds_loaded (list of string) — Document kinds limma_ingest may now name.
  • document_kind_default (string, nullable) — The kind an ingest naming no document_kind is read as in this workspace now. Never null after a successful apply: an apply that would leave no default is refused (409).
  • document_kind_default_conflict (list of dict of string) — {pack, default} for each pack when the packs' own defaults disagree (resolved by document_kind_default_pinned).
  • document_kind_default_before (string, nullable) — The default kind before this apply.
  • document_kind_default_pinned (string, nullable) — The kind the default is pinned to (a document_kind_default this or an earlier apply named); null when it is the packs'.
  • unnamed_sources (integer, nullable) — Current documents that recorded no kind when the apply ran: read as the default kind at every extraction.
  • unnamed_sources_stamped (integer, default 0) — How many of them unnamed_sources: keep recorded document_kind_default_before on.
  • relations_unloaded (list of string) — Relations this pack declared here that the applied version no longer declares, now unloaded: a relation_query on one answers relation_unknown. A relation current rows carry is never unloaded (409, nothing written).
  • vocabulary_unloaded (list of string) — Pack names whose vocabulary was unloaded because the applied version carries none: their term slots no longer expand, and a comparison on one is unbound (vocabulary_unbound), never a narrower answer.
  • document_kinds_unloaded (list of string) — Document kinds this pack declared here that the applied version does not declare: limma_ingest may no longer name them. A kind current documents recorded is never unloaded (409, nothing written).
  • packs_released (list of string) — Pack names this bundle owned here and no longer applies under (a version with no relation sections, or a renamed pack): nothing is left under them and no bundle owns them.
  • dependencies_applied (list of [PackDependencyApplied](#packdependencyapplied)) — Packs this one DEPENDS on, applied first (deepest first) because its slots read their axes — the expert-network pack's geography is the universal core's. Each is a pack in its own right: its relations are addressable here too, documents are read for them, and it stays when this pack is released. A dependency the registry ships at another version is a 409 and nothing is written.
  • labels_added (integer, default 0) — —
  • formulas_added (integer, default 0) — —
  • formulas_overwritten (integer, default 0) — —
  • constraints_added (integer, default 0) — —

limma_attest

POST /v1/public/attest — Produce a JSON-LD certificate attesting the workspace's substrate state

Who may call it. Asks for verify — roles: admin, analyst, service. A key scoped to workspaces reaches only those. A key confined to documents may call it — attests over the key's documents only.

Purpose

Build a signed JSON-LD certificate binding the AI-produced output to the workspace's substrate state.

The certificate is produced with the same signing, canonicalization and refusal semantics as POST /v1/contracts/attest, over the workspace's own Statements as evidence.

The certificate carries:

  • evidence — the Statement ids the workspace held at attestation time (plus coordinates + values at certificate_level='extended').
  • metadata.substrate_fingerprint — the justification-DAG fingerprint of the FULL statement set, so truncated enumeration still anchors everything.
  • metadata.output_sha256 — the canonical hash of the attested payload.
  • proof — signature + fingerprint over the canonical certificate bytes EXCLUDING the proof block itself, the standard JSON-LD/VC shape. A third party re-canonicalizes the certificate minus proof and compares.

An empty substrate is REFUSED (200, attested=false), not certified: an output cannot have come from a verified state that holds nothing, and handing an agent a certificate that implies grounding it does not have is the failure mode this verb exists to prevent.

Over MCP

limma_attest(output: Dict[str, Any], workspace_id: str = '', certificate_level: str = 'standard') -> Dict[str, Any]

Produce a signed JSON-LD certificate binding an output to the workspace's current substrate state, so a third party can independently verify the output came from this state.

certificate_level is standard (evidence statement ids + substrate fingerprint + output hash) or extended (also embeds each statement's coordinate and value).

Returns {certificate, attested, refuse_reason, attest_record_id, source_generation_ids}. Check attested FIRST: a workspace with no substrate is refused, not certified, because an output cannot have come from a verified state that holds nothing. source_generation_ids are the document generations the attested facts were read from (also signed inside the certificate); a later generation that replaces one does not change them, and GET /v1/audit/attestations/{attest_record_id} reads the immutable record back (both null / empty when attested is false). To check the certificate, re-canonicalize it WITHOUT its proof key (json.dumps(..., sort_keys=True, separators=(',',':'))) and compare the SHA-256 against certificate['proof']['fingerprint'].

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • output (dict, required) — The AI-produced output to attest. The certificate cites the workspace state at attestation time, the output payload, and the hash chain so a third party can independently verify the output came from this substrate state.
  • certificate_level (string, default 'standard') — Certificate depth. 'standard' anchors the substrate by fingerprint and enumerates the evidence Statement ids; 'extended' additionally embeds each Statement's coordinate + value, so the certificate can be checked without re-reading the workspace. Anything else is a 422.

Response

  • workspace_id (string, required) — —
  • certificate (dict) — The signed JSON-LD certificate. Empty when attested is false. certificate['proof'] carries the signature + fingerprint; re-canonicalize the certificate WITHOUT its 'proof' key to check it.
  • attested (boolean, default True) — False when no certificate could be issued — e.g. the workspace holds no substrate Statements, so there is nothing for the output to be attested AGAINST. Read refuse_reason.
  • refuse_reason (string, default '') — Machine-readable reason when attested is false.
  • attest_record_id (string, nullable) — The id of the attest record this call wrote; read it back at GET /v1/audit/attestations/{attest_record_id}. Null when attested is false (nothing was attested, nothing recorded).
  • source_generation_ids (list of string) — The SOURCE GENERATION ids (source_id of each document generation) whose facts the attested substrate holds — every statement metadata.substrate_fingerprint binds, including those past the enumerated evidence when evidence_truncated — sorted. Also inside the signed certificate (metadata.source_generation_ids), so the signature covers them. A later generation — a re-connect or re-ingest that replaces one of them — does not alter the record or the certificate: they keep naming the generation that was attested. It licenses 'this certificate was issued over facts read from these generations'; it does NOT license that those generations are still current (read /v1/public/generations).

limma_state

GET /v1/public/state — Return the workspace's Statement set with statuses

Who may call it. Asks for any key — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — the statements of the key's documents; sources_unreadable names the rest.

Purpose

Return the workspace's current Statement set with statuses.

What the workspace knows at this moment, over the documents your key may read — the MCP tool limma_state calls this verb. limit bounds the statements returned (default 100, max 1000); total is how many this response holds.

Over MCP

limma_state(workspace_id: str = '') -> Dict[str, Any]

Return the workspace's current Statement set with statuses — the substrate state right now (what the agent can ground answers in).

Also carries entity_duplicate_proposals: pairs of names that MAY be one entity and have not earned a merge. Nothing applies them — a merge needs corroboration from a row (same subject, relation, period, every other shared slot equal), because names alone are not evidence: "Ferrum Systems AG", "Ferrum Systems Pvt Ltd" and "Ferrum Systems Ltd" share a brand and are three real companies. Merges that WERE licensed are already applied and show up as one row with two statements.

Request

  • workspace_id (string, nullable, query) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • limit (integer, query, default 100) — —

Response

  • workspace_id (string, required) — —
  • statements (list of [StateStatement](#statestatement), required) — —
  • total (integer, required) — —
  • dag_fingerprint (string, nullable) — —
  • sources_unreadable (list of string) — —
  • extraction_quality_per_page (dict of number) — —
  • registrants (dict of string) — —
  • grid_accounting (dict of [GridAccountingSummary](#gridaccountingsummary)) — —
  • pages_not_read (dict of list of [PageNotRead](#pagenotread)) — {source_id: [PageNotRead]} — the pages of each document a reader REFUSED (reason says why; page_number null: the whole document). []: no reader refused a page of that document. A source ingested before the record existed is ABSENT, never []. Licenses: in a document with a record, an absence on that page (anywhere, when null) is not evidence the fact is not there. Does NOT license that a document with [] had every fact extracted — only that no reader refused a page. Restricted to the sources your key may read.
  • merging (dict, nullable) — Present once an identity block was stated for the workspace (connect): WHICH RECORDS ARE ONE THING, per entity type, as your key may see it. {status, run_id, resolved_at, types: {<entity type>: {status, entity_type, unit, merged_entities, merged_records, proposals}}, entities: [{entity_type, resolved_id, members}], listed: {entities, proposals}, decisions}. entities lists the records merged as one (the first 500; each type's merged_entities is the count) — to erase one real thing, erase every member. A type's proposals counts the pairs only PROPOSED as one: they are listed under entity_duplicate_proposals (kind: entity, the first 500) and merge nothing. A status other than complete means every reply counts one record as one entity (of that type). decisions is how many operator decisions are held (resolve).
  • entity_duplicate_proposals (list of dict) — An entry with kind: entity is a PAIR OF RECORDS of one entity type that may be one thing and has not earned a merge: {kind, entity_type, left, right, left_name, right_name, verdict, why, evidence, vetoes}; verdict says how weak the evidence is (same_name, one_shared_fact, identifier_unconfirmed, identifier_names_differ, conflicting_evidence, …). Answer one with resolve. Every other entry is a pair of NAMES that may be one entity and have not earned a merge: {left, right, suggested_canonical, verdict, why}. Nothing applies these: they are the list a human resolves. A merge is licensed only by CORROBORATION FROM A ROW — the same subject, relation, period and every other shared slot equal — because names alone are not evidence: 'Ferrum Systems AG', 'Ferrum Systems Pvt Ltd' and 'Ferrum Systems Ltd' share a brand and are three real companies, and folding them would delete two employers with nothing downstream able to notice. A pair whose legal forms CONFLICT never appears here at all — that is decided, not pending. Merges that WERE licensed are already applied and are visible as one row with two statements; this list is what is left over. Empty means no unresolved look-alikes, not that none were looked for.
  • row_merges (list of dict) — ROWS THAT GAVE WAY to a statement of the same fact that said more. {from_row_id, into_row_id, reason, merged_at}. When two documents state one role — one naming its industry, one not — the richer statement carries the fact and both documents' receipts, and the thinner row is retired. If you hold a row_id from before the merge, find it in from_row_id and follow into_row_id. A merge needs every slot both statements share to AGREE: statements that disagree are kept side by side, never merged.

limma_facts

POST /v1/public/facts — Return the RELEVANT FACTS for a question, without answering it

Who may call it. Asks for read — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — delivers facts of the key's documents only; coverage.sources_unreadable names a document that was withheld from the answer.

Purpose

The retrieval verb: every relevant fact, with provenance, no answer.

Cheaper than /v1/public/ask by the whole answer machine, and the right call for an agent that wants to reason over the evidence itself rather than receive a conclusion.

Over MCP

limma_facts(question: str, workspace_id: str = '', limit: int = 100, offset: int = 0, depth: str = 'ranked', cursor: Optional[str] = None, mode: str = 'auto', coordinate: Optional[Dict[str, str]] = None, output_mode: Literal['full', 'compact'] = 'full', relation_query: Optional[Dict[str, Any]] = None, source_ids_limit: Optional[int] = None, subject_sources_after: Optional[str] = None, sources_unreadable_after: Optional[str] = None, labels_offset: Optional[int] = None, labels_limit: Optional[int] = None) -> Dict[str, Any]

Return every RELEVANT FACT for a question, WITHOUT answering it.

READ complete FIRST: false means NOT every relevant fact — never say "all", "only N" or "none"; incomplete_because says why.

HOW MUCH OF EACH FACT YOU RECEIVE — output_mode, and it is a COST control. compact is what an agent REASONS with: the fields it acts on — handle, kind, provenance, source_id, the address (entity / attribute / period), value and the printed value_token, the byte receipt (page / char_start / char_end / byte_verified), the disagreement bits (contested / contested_checked / contested_scope / distinct_magnitudes / competing_readings) and relevance_score — with values identical to the full form's. full (default) additionally carries the prose joins (states_values, stated_by, stated_numbers, uses_defined_terms), the verbatim span and display text, the reading discriminators and the aggregate roles (aggregate_role / part_of / parts and their handles). A full fact is roughly 700–1,300 tokens, and an agent re-sends every fact it holds on every turn. Use compact to discover addresses, choose values and check receipts; use full when you need a clause's numbers, a number's clauses, or a row's place in the document's own arithmetic. coverage, gaps, enumeration and next_cursor are unchanged by it, and handle joins the two forms.

BOTH forms carry slots — A RECEIPT PER STRING SLOT of a categorical fact ({"title": {value, page, char_start, char_end, byte_verified, reason}, ...}). A title, a company or a geography is a string, and the fact-level page / char_* receipt locates a NUMBER; each slot here is located exactly once inside the fact's own attested sentence / row span and re-slices through /v1/public/region like any other receipt. byte_verified false names why in reason: no_enclosing_receipt (the fact has no attested span), absent (not printed whole inside the span — a piece of a longer word or of a longer number, "5" inside "5,000", is not printed), ambiguous (printed twice in the span — which one is this slot's cannot be said), empty_surface, unprintable_surface (the extractor stored a typed value, not a printed string — shown as its JSON rendering, never searched for) or attestation_error (the page layer failed for this fact; logged on our side). Every slot the claim declared is present; {} means the claim declared no slots, never that they could not be checked. value is the declared surface with whitespace runs collapsed, and the page may wrap it — to redeem a slot, collapse whitespace on the region text before comparing (" ".join(text.split()) == value); a plain == is False on a wrapped title with a true receipt. Slots live on kind == "claim" facts; a kind == "semantic" clause is a sentence, not a relation row, and declares none.

TWO KINDS OF ANSWER, and they differ in kind — choose with mode:

  • mode="auto" (default) ENUMERATES a relation your question names; otherwise it RANKS — and when your question means countable addresses, address.suggested lists them FIRST with their counts: call again with mode="enumerate" and the coordinate you MEAN (a value_kind is every value of a kind; a clause_type or attribute is one filing label). available lists every address held.
  • mode="explore" PINS relevance-RANKED retrieval. It finds things that look like your question and CANNOT tell you how many exist: a ranked list has no denominator. Use it to DISCOVER which addresses exist — read facts[].attribute / period / entity on what comes back.
  • mode="enumerate" returns a CLOSED SET at an ADDRESS you name in coordinate — e.g. {"attribute": "revenue", "period": "FY 2019"} (numeric axes attribute / period / entity; prose axis clause_type; value axis value_kind — a kind in available (date, identifier…), EVERY such value printed, value normalised, a null page redeemed without one; source_id narrows to one document). It returns every fact there plus enumeration.exist, the count in the store BEFORE any page limit, so exist == delivered licenses the sentence these are all of them and exist == 0 on a bound address licenses there are none. enumeration.bound false means the address itself does not exist in this workspace (the store may name the concept differently — net_sales is unbound where the store says revenue; discover the name with explore first). coordinate is REQUIRED in this mode (422 otherwise).
  • mode="enumerate" with relation_query instead of coordinate (never both — 422) returns the CLOSED SET of typed RELATION ROWS — categorical facts such as held_role / screening / engagement — as relations: {"relation": "held_role", "where": [["seniority", "in", ["VP", "SVP"]], ["geography", "eq", "Germany"]], "active_in": 2019, "entity_id": null}. Ops are eq / ne / in / gte / lte (gte/lte on number and date slots only); contains / like / regex are refused because a substring match has no closed set. active_in(Y) is the half-open rule period_from <= Y-12 and (period_to is null or period_to > Y-01) — a role ending 2019-01 is NOT active in 2019. WHAT relations.count LICENSES: it is every current matching row in the store over your visible scope, so count == delivered with bound: true licenses these are all of them and count == 0 with bound: true licenses there are none. WHAT IT DOES NOT: bound: false means nobody looked — count is null, never 0 — and not_recorded lists the entities never looked at for this relation (with entity_id, that entity; workspace-wide, every known entity without a coverage row, and then coverage.withheld_because is narrowed: the count says nothing about them). ABSENCE IS A ROW: relations.absence is the PER-ENTITY verdict — absence.entity for an entity_id query (null when that entity holds rows), and workspace-wide absence.split plus BOTH lists absence.none / absence.not_recorded, each verdict {entity_id, kind, bound_by, denominator, coverage, corrected_rows}. kind: "none" licenses exactly no row of this relation is recorded for this entity in source bound_by as of the coverage row's as_of — a coverage row is the evidence (coverage lists it with its receipt and declared_by: source when a document's own structure, such as an export column, declared it, extractor when an extraction pass did) and denominator is 0; it says nothing about sources that did not cover the entity. kind: "not_recorded" licenses NOTHING about the relation for that entity — no load declared it looked, or every source that did states a row a correction retired (corrected_rows names them) — bound_by and denominator are null, so never read it as "none" and never as "clean" (a missing screening is not a passed one). none counts rows of the relation, not rows matching where. Likewise relations.undated > 0 under active_in withholds the licence (narrowed_by: undated_rows): rows with no period_from cannot be active in any year, so a zero beside them is not "there are none" — counted there whatever period clause the where adds, and not again under period_unjudged. Term and entity_ref values are case-folded on both sides; a date value names the WHOLE of what it names under every op (eq "2021" is every row dated in 2021, ne "2021" every row dated outside it, lte "2021" reaches 2021-12; a day names only itself); and period_from / period_to are addressable in where on YYYY-MM values beside the declared slots. A clause on a TERM slot is EXPANDED through the workspace's vocabulary before it runs ("Head of" → Director via synonym; ["seniority", "gte", "VP"] → the ranked set; a region → its members) and relations.expansion prints each resolution; not_in is the negated set op. A term slot whose axis has NO vocabulary — or a ne / not_in negating a surface the vocabulary uses for two terms, whose complement would omit rows under either reading — is named in relations.unbound_slots: an equality clause on it ran AS WRITTEN (exact spelling) and the licence is withheld with narrowed_by: vocabulary_unbound:<slot>; a comparison on it cannot run at all and the query is unbound — as is a gte / lte whose value the order does not know (the set above an unknown surface is undefined). An eq / in value the vocabulary does not know ("England" where it lists only the United Kingdom) matches AS WRITTEN beside the known values and is named in relations.unknown_surface_slots (narrowed_by: vocabulary_unknown_surface:<slot>) — never a licensed "none". Rows whose stored value is a SURFACE of a term ("Vice President") are matched as that term; rows whose stored value no term places, or that sit at a coarser term than you asked, are named in relations.unplaced_slots (narrowed_by: vocabulary_unplaced:<slot>) and the licence is withheld. Each row carries slots, period_from / period_to / as_of, and its STATEMENTS: sources lists every document in your scope that states the row (source_id, receipt with byte_verified, slot_receipts, provenance_kind, confidence), stated_by counts them (2 = corroborated by two documents), and the flat source_id / receipt / slot_receipts are the first statement's. facts is empty on this lane; page with offset against relations.count while relations.truncated is true (rows remain past this page; the next page starts at offset + delivered). coverage.paging.has_more says the same on BOTH enumerate lanes (the read goes one row past the page, so it is true exactly when rows remain and next_offset is the next page's offset); next_cursor is null on the enumerate lanes — page by offset.

The pattern for a count you can act on: explore to find the address, then enumerate at it. Everything below describes the explore response; the enumerate response carries the same facts shape plus enumeration.

It runs retrieval alone — no answer machine — for an agent that reasons over the evidence itself.

Returns {facts, coverage, gaps}:

  • labels — the names your documents file facts under that share a word with your question (labels_offset / labels_limit page the whole map), with documents and the spellings to address (case/punctuation/plural variants are one name; a similar name is a counted proposal, merged only once accepted, as an aliases entry); map_version moves when a name you see changes.
  • facts — each carries its COORDINATES (entity / attribute / period) and byte-addressed provenance (source_id, and where resolvable a page + character range you can re-slice to confirm the value without trusting us). Ranked by relevance_score. A fact from the table layer may also carry aggregate_role — what the document's OWN ARITHMETIC proves it to be: total (its value is the proven sum of the rows above it, listed in parts) or component (it is one of the rows summed into the total named in part_of). Proved from layout, so it tells you what a figure IS even when nothing recognises its label; absent means the row is in no proven identity, not that it is unimportant. part_of / parts are LABELS, for reading; part_of_handle / part_handles are the matching handle values, so you can JOIN the relation onto the very facts in this response instead of matching on a label two tables may share. part_handles is positionally aligned with parts.
  • facts[].contested — READ THIS BEFORE ACTING ON A VALUE. True means the substrate holds COMPETING readings: at that coordinate, or in another statement of the document judged to state the same property; competing_readings lists them with receipts and distinct_magnitudes counts the different numbers after folding sign and power-of-ten scale (1 with several readings: the digits agree, the SCALE differs). A False is a FINDING only within contested_scope (same_address, judged_pairs, both, or none); contested_checked is false exactly when it is none, and then contested says nothing.
  • facts[].states_values / facts[].stated_by — THE PROSE <-> NUMBER JOIN. A prose fact (kind == "semantic") lists the numbers its sentence states; a numeric fact (kind == "claim") lists the sentences that state it, with the clause text. Use it to attach a number to the language qualifying it instead of re-parsing prose yourself. BRANCH ON relationship — three values, strongest first. states:containment means the value's span nests inside this clause AND the clause's text prints a number equal to the value (deterministic, no model judged it; matches powers of ten, so it establishes the MAGNITUDE's location, not the exact bytes or the scale). states:containment_unverified means the spans nest but the clause does NOT print that number, or there was nothing to check with — a retrieval hint, never provenance, because the value offsets are recorded by the extractor and are not re-sliceable. qualifies:adjacency means only that this was the nearest clause to a value no clause contained, and is never evidence that the sentence states the number. AN EMPTY LIST IS AMBIGUOUS AND THE FACT CANNOT RESOLVE IT: it means "no edge" on a workspace whose join was derived, and "never computed" on a workspace whose join was never derived (one ingested before the join existed). limma_facts answers it for you: coverage.prose_edges_derived is true / false / null, and gaps carries prose_edges_not_derived when it is false. Check that BEFORE reading an empty join as a fact about the document.

THEN READ confirmation — IT IS WHERE THE SCALE IS OR IS NOT PROVEN. states:containment is power-of-ten tolerant, so on its own it cannot tell you whether the sentence printed the value's digits or the value divided by a million. confirmation is {rule, factor, printed, occurrences, exact}, recomputed at delivery, with rule one of: exact_digits (a token in the clause equals the value exactly); rounded (same scale, different digits, matched only on the 0.5% tolerance); power_of_ten (the clause prints the value divided by factor — 1e3/1e6/1e9 — so THE MAGNITUDE IS LOCATED AND THE SCALE IS NOT PROVEN); unconfirmed (nothing grounds it, or there was nothing to check with). printed is the token the document actually prints, so you can compare it to value yourself — but the match is SIGN-BLIND, so the identity is printed * factor == abs(value): a stored -499,000,000 is confirmed by a printed 499 at factor 1e6, because accounting text spells a negative "(499)". If the sign matters, read clause_body. occurrences > 1 means the clause prints several tokens matching at that factor and cannot say which it meant. exact is a SEPARATE bit from rule and you need both: it is False when the 0.5% band alone bought the match, which is how a printed 104,262 "confirms" 104,347,000,000 and a bare year 2022 "confirms" 2,023,700,000 — under both rounded and power_of_ten, so filter on exact rather than on rule to find them. CAVEAT, AND IT IS THE POINT OF THE FIELD: confirmation speaks about DIGITS AND SCALE ONLY. It never says the value sits on the right entity/attribute/period, and it never certifies anything; it is SOURCED delivery metadata. If you are about to act on a number whose rule is power_of_ten, read clause_body and resolve the scale yourself.

  • facts[].stated_numbers — THE NUMBERS A PROSE FACT'S OWN TEXT PRINTS. The prose layers carry no value, and they are where the flattened financial statements arrive — a clause such as "Cash and cash equivalents $ 19,334 $ 20,522" is otherwise untyped text. Each entry is one row-slot of that text: {label, value, value_token, period, period_binding, unit, char_start, char_end}. value carries the table's scale ("in millions") except on per-unit rows, where it deliberately does not — an EPS is not multiplied by a million. value_token is what the document printed. char_start/char_end index THIS FACT'S verbatim_span, not the document. BRANCH ON period_binding: column means the value sits in the column its header owns (geometric); column_ordinal means placeholder columns were dropped and the binding is by ordinal, so a column shift would mis-align it; "" means the table declared no header and NO period is claimed. DISTINCT FROM states_values, which is a join to a stored claim and needs the number to exist as a substrate row; this needs only the text you were already given. Read stated_numbers_truncated — non-zero means the list was cut at the per-fact ceiling and is NOT the whole table. Everything here is retrieval evidence with a coordinate, never a certification, and the walk is scope-blind: a cover page's Washington, D.C 20549 and a row's footnote marker (Basic earnings per share (2) $ 0.60 types (2) as -2) are typed too, so read label before you bind.

TWO TEXT FIELDS, AND THE DIFFERENCE MATTERS. verbatim_span is text that is IN THE DOCUMENT: it is present only when verbatim_page / verbatim_char_start / verbatim_char_end locate it uniquely and re-reading that region reproduces it byte for byte — so you can verify it yourself, and a null means we could not prove it rather than that no text exists. display_text is OUR rendering of the fact (label, value, coordinates) and makes no such claim. Never quote display_text as a document quotation. * facts[].byte_verified / value_token / page / char_start / char_end — THE NUMBER, WHERE IT IS PRINTED. When byte_verified is true, re-slicing (page, char_start, char_end) of that source returns value_token exactly. value_token is the token AS PRINTED ("74,452", "(449.9)"); value is the stored, scale-normalised number and is a different thing — a fact can have a byte-true token and a wrong value, which is precisely what this lets you catch.

attestation_method names the rule and is worth branching on. Located on a fact: span / row / page / label, strongest first — every one of them ties the location to THIS fact (by an enclosing proven span, or by the row label the document itself prints), not merely to the digits. Not located: absent means NO printed form of that number occurs in the document at all — treat the value as unconfirmed extraction, it is the strongest warning this API emits; ambiguous means it occurs but could not be pinned to this fact's row; short_token and derived mean we did not look (a one-digit number, and a computed value that is in no document by construction). structured_citations on the warrant carries two MORE values a fact never does: document (unique in the whole source, with nothing to corroborate it — the weakest thing here, discount it accordingly) and clause (a prose region locating the clause body rather than a number). A fact pulled from a TABLE (limma_connect) has no page: structured_row means its own row and column (db_table / row_key / db_column) were re-read now and equal value_token — no digit floor, so a 7 verifies; row_changed means the cell now holds another value or NULL (the import is stale); row_unreadable means the row could not be read in this process. * coverage — READ THIS. facts_dropped_by_cap > 0 means you are holding a ranked PAGE of the relevant set, not all of it; sources_searched versus sources_in_workspace tells you how much of the workspace was consulted; aggregate_relations is how many IS-PART-OF relations this document proved at all (0 = its tables never closed arithmetically). embedding states whether the prose layer was ranked by MEANING or by tokens alone: refused > 0 means persisted clause vectors came from a DIFFERENT embedding model than this deployment's and could not be compared, so the prose ranking silently degraded to lexical. vector_rows_capped > 0 means the per-question vector read was bounded and those clauses were ranked lexically. retrieval_order names HOW facts is ordered, which matters whenever limit is small. layer_score sorts by raw retrieval score across four layers that score on incomparable scales, so claims crowd the top and a relevant prose clause or table cell can sit below every claim however relevant it is. unified_cross_layer takes each layer's best in turn, so every layer reaches the top of the page. It re-orders the SAME POOL — facts_scored and whole-pool recall cannot move — but at any limit below facts_scored you RECEIVE A DIFFERENT SET, which is the only case where this field matters. Treat its retrieval-QUALITY effect as UNESTABLISHED. * gaps — retrieval layers that returned nothing. An empty prose layer means the narrative had nothing on this question, which is a fact about the document rather than a failure to look. ONE entry breaks that rule and says so: embedding_vectors_refused is NOT about the document — it reports a model mismatch inside the retrieval itself, a deployment fault. Treat a prose result accompanied by it as under-ranked, not as evidence of absence. * next_cursor / depth / offset — HOW TO REACH THE REST OF THE SET, and read this before concluding a fact is not in the document.

limit is NOT the ceiling on what you can reach. Retrieval caps each of its four layers (55 claims + 28 clauses + 16 passages + 30 cells = 129 facts) and limit reaches 200, so the default pool is ALREADY fully deliverable in one call — the facts you cannot address are the ones those LAYER caps dropped.

depth="matched" pages the complete matched set instead. It costs a wider retrieval, so it is opt-in — but it is the only way to address a fact below the cap. DECLARE IT ON THE FIRST CALL: a traversal cannot change depth half-way without skipping facts.

Then page with cursor, not with offset. The cursor pins the question, the depth, the ordering and the position, so successive pages are exact slices of ONE list. A raw offset is exact only if you hold the depth fixed yourself: the delivery order is a descending sort over four layers that score on incomparable scales, so a deeper pool is a different PERMUTATION of a superset rather than an extension of it. If the workspace changes between pages the cursor is REFUSED rather than silently paging a re-ordered list — re-request the first page.

coverage.paging.deeper_pool_available is true when facts exist that the current depth cannot address at any limit. A null next_cursor means THIS POOL is exhausted, which is not the same as "you have every relevant fact" — check that field too.

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • question (string, required) — What the facts should be RELEVANT TO. Retrieval is relevance-ranked against this text; it is not answered.
  • limit (integer, default 100) — How many facts to return, ranked by relevance. Capped at 200. THIS IS THE PAGE SIZE, NOT THE CEILING ON WHAT YOU CAN REACH — see offset / cursor and coverage.paging.
  • offset (integer, default 0) — WHERE IN THE RANKED SET THIS PAGE STARTS. 0 is the top. Paging is exact only within ONE depth: the delivery order is a descending sort over four layers that score on incomparable scales, so a deeper pool is a different PERMUTATION of a superset, not an extension of the shallower one. Prefer cursor, which pins the depth and the ordering for you and refuses rather than skip.
  • depth (one ofranked,matched`, default'ranked') — WHICH POOL TO PAGE.ranked(the default) is retrieval's per-layer caps — 55 claims + 28 clauses + 16 passages + 30 cells = 129 facts. Sincelimitreaches 200, that pool is ALREADY fully deliverable in one call, so pagingrankedgains nothing: the facts you cannot reach are the ones the LAYER CAPS dropped, andcoverage.facts_dropped_by_layer_capcounts them.matched` pages the complete matched set instead, which is the only way to address a fact below the cap. It costs a wider retrieval and is opt-in for that reason. DECLARE IT ON THE FIRST CALL: a traversal cannot change depth half-way without skipping facts, so a cursor pins it.
  • cursor (string, nullable) — An opaque token from a previous response's next_cursor. It PINS the question, the pool depth, the ordering and the offset, so successive pages are exact slices of ONE list — no overlaps, no gaps. It overrides offset and depth. It carries no authority: the document ACL is re-enforced from your API key on every page. If the workspace changed between pages the cursor is REFUSED (409 cursor_stale) rather than silently paging a re-ordered list — re-request the first page.
  • scope (dict of string, nullable) — Reserved for source scoping; not yet honoured.
  • mode (one ofauto,explore,enumerate`, default'auto') — WHICH KIND OF ANSWER YOU WANT, and they are different in kind.auto(the default) gives you the STRONGEST form available for your request: it enumerates when your question names an address this workspace actually has, and falls back to ranked retrieval when it does not — telling you which happened inmodeand why inaddress. It never guesses between two equally-good addresses; a tie falls back and names both, because a closed set at the WRONG address is a licensed sentence about a question you did not ask.explorepins ranked retrieval: it finds things that look like your question and CANNOT tell you how many exist, because a ranked list has no denominator.enumeratereturns a CLOSED SET at an address you name incoordinate: every fact there, plusenumeration.exist— the count in the store before any page limit — soexist == deliveredlicenses the sentence *these are all of them*, andexist == 0on a bound address licenses *there are none*. Sendexplorewhen you want candidates to read rather than a set to count; sendenumerate` when you are naming the address yourself and want the request REFUSED (422) rather than silently answered by ranking if the address is missing.
  • coordinate (dict of string, nullable) — THE ADDRESS to enumerate. Required when mode=enumerate (422 otherwise — an enumeration with no address would report a denominator over the whole workspace while looking like a closed set). Numeric axes: attribute, period, entity. Prose axis: clause_type. Value axis: value_kind — date, money, percentage, duration, quantity (a figure and a physical unit), identifier (a reference mixing letters and digits: an invoice, order or account number), email, url or phone: EVERY mention of that kind of value printed in the documents (each a fact of kind: value whose value_token is the bytes as printed, value the normalised form — null where the bytes do not decide it: a 05/06/2026 date, a phone printed with no country code — and page/char_start/char_end its limma_region receipt); the kinds a workspace holds are listed in coverage.available_coordinates as value_kind:<kind>. Naming axes of two of these stores is a 422: one closed set cannot span them. source_id narrows any of them to one document.
  • output_mode (one offull,compact`, default'full') — HOW MUCH OF EACH FACT TO SEND, and it is a COST control, never a capability one.full(the default) is the whole DeliveredFact — 52 fields, roughly 700–1,300 tokens per fact, most of it the prose joins (stated_by,stated_numbers,uses_defined_terms) and the aggregate roles.compactprojects every fact to the 27 fields an agent branches on: handle, kind, provenance, source_id, entity, attribute, period, value, value_token, page, char_start, char_end, byte_verified, contested, contested_checked, contested_scope, distinct_magnitudes, competing_readings, relevance_score, verbatim_page, verbatim_char_start, verbatim_char_end, display_text, canonical_clause_type, states_values, slots, value_kind — with IDENTICAL values and contracts (read them on the DeliveredFact schema;handlejoins back to the full form), except that adisplay_textover 300 characters is cut and ends…(the whole text: its receipt throughlimma_region, or the full form). An agent that re-sends every fact it holds on every turn pays that size on each turn. Ask forcompactwhen you will reason over addresses, values and receipts; ask forfullwhen you need the prose<->number join or a fact's role in the document's own arithmetic.coverage,gaps,enumeration,modeandnext_cursor` are unchanged by it.
  • relation_query ([RelationQuerySpec](#relationqueryspec), nullable) — THE CATEGORICAL ADDRESS, beside coordinate and mutually exclusive with it (422 if both). Requires mode=enumerate (422 otherwise — a relation query IS a closed set, and in explore it would be a filter that does not filter). Returns the closed set in relations — every matching row with its receipt and slot receipts, count (the store's number BEFORE the page limit, over your full visible scope), bound and not_recorded — with enumeration.exist / coverage.completeness_licensed computed by the SAME rule the coordinate path uses. facts is empty on this lane: relation rows are typed records, not ranked spans, and live in relations.rows. Page with offset / limit; relations.count is the denominator and relations.truncated says whether rows remain PAST this page (offset + delivered < count; the next page starts at offset + delivered) — false on the last page. enumeration.truncated is the licence's notion instead (THIS response does not hold the whole set), true on every page of a multi-page set, the last included, which is why the licence is withheld on each. coverage.facts_delivered / coverage.paging describe THIS page of rows; the read goes one row past the page, so coverage.paging.has_more is true exactly when rows remain (it equals relations.truncated, with next_offset the next page's offset) and pool_size counts that look-ahead row. next_cursor is null on the whole enumerate lane (a closed set is paged by offset, not by a pinned ranking).
  • source_ids_limit (integer, default 20) — How many document ids each per-document list in this reply holds: coverage.demands[].subject_source_ids and coverage.sources_unreadable. Default 20, at most 1000. Each list comes with its total, a truncated flag and the cursor for its next page.
  • subject_sources_after (string, nullable) — Page coverage.demands[].subject_source_ids: pass the subject_sources_next_after of the previous reply. Ids come in id order, after this one.
  • sources_unreadable_after (string, nullable) — Page coverage.sources_unreadable: pass the sources_unreadable_next_after of the previous reply. Ids come in id order, after this one.
  • labels_offset (integer, nullable) — PAGE THE WHOLE MAP of names (labels) instead of the default — which lists only the names sharing a word with your question (at most 10). From this offset of the ranked map; labels.next_offset continues it.
  • labels_limit (integer, nullable) — Names per page of the map (default 25, at most 100); sending it alone pages from 0.

Response

  • complete (boolean, default False) — READ THIS FIRST. True only when this reply holds EVERY fact at the address it names (coverage.completeness_licensed, the same bit). False means you may NOT tell anyone this is all of them, that there are only N, or that there are none — incomplete_because says why and what to call instead.
  • incomplete_because (list of string) — Why complete is false, in plain sentences, most important first (at most five; the last then counts the rest). Empty when complete is true. A ranked reply says it is a sample that cannot count, names the addresses this workspace CAN count, and points at limma_search with idea for passages.
  • workspace_id (string, required) — —
  • question (string, required) — —
  • facts (list of [CompactFact](#compactfact) \| [DeliveredFact](#deliveredfact)) — The relevant facts, ranked by relevance_score. Identical shape to warrant.facts from /public/ask, so results join across the two verbs; every per-field contract is on the DeliveredFact schema. With output_mode=compact each fact is the CompactFact projection — the same values on 27 of those fields, nothing recomputed. TWO FIELDS DECIDE WHETHER YOU MAY ACT ON A NUMBER. contested says the substrate holds competing readings for that coordinate or a judged same-property statement of its document — READ IT WITH contested_scope: a contested: false is a finding ONLY about that scope, and contested_checked is false (nothing was checked) exactly when the scope is none. And on the prose<->number join (states_values/stated_by), READ confirmation AS WELL AS relationship: states:containment is power-of-ten tolerant, so it cannot by itself separate a sentence printing the value's digits from one printing that value divided by a million. confirmation.rule is exact_digits, rounded (same scale, digits matched on the 0.5% tolerance alone), power_of_ten (magnitude located, SCALE NOT PROVEN, with factor naming the power) or unconfirmed. printed is the token the document prints; occurrences > 1 means the clause cannot say which matching token it meant; exact is a SEPARATE bit, False when the 0.5% band alone bought the match (it can be False under more than one rule — filter on exact, not on rule). It is recomputed at delivery, it speaks about DIGITS AND SCALE only — never about whether the coordinate is right — and it is SOURCED metadata that certifies nothing.
  • coverage (dict) — {facts_scored, facts_delivered, facts_dropped_by_cap, layers, aggregate_relations, sources_in_workspace, sources_searched, embedding, retrieval_order, prose_edges_derived}. Read it: prose_edges_derived is the prose<->value join's own state for this workspace — true (derived), false (never derived, so every empty states_values/stated_by on this response is a NON-ANSWER) or null (the check could not be run). facts_dropped_by_cap > 0 means this is a ranked PAGE. aggregate_relations is how many IS-PART-OF relations the bound document proved (0 = its tables never closed arithmetically, so empty aggregate_role fields are the document's fact, not a miss). retrieval_order names HOW facts is ordered: layer_score = descending raw retrieval score, which is a layer preference as much as a relevance one (the four layers score on incomparable scales, so claims crowd out prose); unified_cross_layer = the cross-layer fusion, which takes each layer's best in turn so prose and cells reach the top of the page instead of sitting under every claim. It re-orders the SAME POOL, so facts_scored and whole-pool recall cannot move — but at any limit below facts_scored you RECEIVE A DIFFERENT SET, which is the only case where this field matters. embedding states whether the prose layer was ranked by MEANING or by tokens alone: {active, model_version, dimension, query_embedded, weight_per_query_token, clauses_scored, compared, no_stored_vector, vector_column_present, vector_rows_capped, refused, by_status}. refused > 0 means persisted vectors came from a DIFFERENT embedding model than this deployment's and could not be compared — the prose ranking degraded to lexical, and that is a deployment fault, not an absence in the document. Three fields keep different causes of that same symptom apart: vector_column_present false = a store SCHEMA with no vector column (an older store, not a misconfiguration); query_embedded false = nothing survived the model-identity check, so no query vector was produced and model_version is the model this deployment WOULD have used; vector_rows_capped > 0 = the per-question vector read hit its bound and those clauses were ranked lexically only. paging: {depth, order, offset, delivered, pool_size, has_more, next_offset, facts_matched, pool_truncated, deeper_pool_available, deeper_pool_depth}. READ deeper_pool_available — true means facts exist that THIS depth cannot address at any limit, and depth: "matched" reaches them; has_more means this depth has more pages and next_cursor fetches the next one — except with mode: "enumerate" (both lanes), where has_more is true exactly while rows remain, next_cursor is always null and you page by next_offset (the read goes one look-ahead row past the page: pool_size counts it, facts_scored and facts_dropped_by_cap do not). facts_matched is a FLOOR, not a count: the passage layer records its matched after a cap has already bitten, so the total under-reports that layer. Read it as 'at least this many'. Lists of document ids are ONE PAGE (source_ids_limit): demands[].subject_source_ids (the documents each demand was scoped to) and sources_unreadable (the documents a key confined to documents may not read) each come with <name>_total, <name>_truncated and <name>_next_after — here <name> is subject_sources / sources_unreadable; send the last back as subject_sources_after / sources_unreadable_after for the next page.
  • next_cursor (string, nullable) — Pass this back as cursor to get the next page. Null means this pool is exhausted at this depth; that is NOT the same as 'you have every relevant fact' — check coverage.paging.deeper_pool_available, which is true when the layer caps dropped facts a deeper traversal can still address.
  • gaps (list of string) — Honest absences — retrieval layers that returned nothing for this question. A layer missing from coverage.layers is not noise: it says where the substrate had nothing to offer. FOUR entries are NOT about the document. embedding_vectors_refused reports a model mismatch in the retrieval itself. prose_edges_not_derived says the prose<->value join was NEVER DERIVED for this workspace — so every states_values / stated_by on this response is empty because nothing was computed, NOT because the clause states no number. It appears only when a semantic or claim fact was actually delivered, and only when the check could be run: read coverage.prose_edges_derived (true / false / null) for the raw bit, where null means the check itself was unanswerable and the empty joins mean nothing either way. definition_edges_not_derived says the same thing about the clause<->definition join: every uses_defined_terms on this response is empty because nothing was computed, NOT because the clauses use no term this document defines. It appears only when a semantic fact was delivered; the raw bit is coverage.definition_edges_derived. semantic_extraction_off is not about the document either: it rides beside a semantic gap when this deployment ingests with no working embedder, so the prose-clause layer was never built and that empty layer says nothing about what the documents state; coverage.semantic_extraction carries the state (on / off / unknown) and its cause.
  • mode (string, default 'explore') — Which mode PRODUCED this response — always explore or enumerate, never auto. A request that defaulted reads this to find out which kind of answer it got; address.resolved_by says who chose. explore responses carry no enumeration.
  • address (dict, nullable) — WHICH ADDRESS THIS QUESTION RESOLVED TO, and why. {resolved_by, relation, entity_id}, plus kind (relation, clause_type — a prose topic, attribute — a numeric one, or value_kind) and, for the last three, coordinate — exactly what the enumeration read, re-sendable as coordinate. When nothing bound: refused_because and available — EVERY address this workspace HAS, of every kind (a relation with its slots; any other as {kind, name, coordinate}), so a refusal is one precise call away from the answer instead of a dead end. resolved_by: caller (you named coordinate or relation_query), question (it was read off your question, and considered shows the addresses weighed, best first, and the words that scored — at most 10, considered_total counts them), none (this is a ranked response). A topic, attribute or value kind is never bound from the question: refused_because: suggested_not_bound and suggested — the addresses your question MEANS, best first, each {kind, name, coordinate, matched_on, exist, meaning, covers} (coordinate carries source_id when the question names ONE document; documents lists those it names; covers: how many of the documents it names the address reaches). ranking: by: meaning (with model, unscored, centroids) or by: words with because (no embedder). refused_because: no_relation_schema (this workspace has no address of any kind — stop trying to address it), no_relation_matched (it has some and your question named none — rephrase using available), ambiguous_relations (two matched equally well, or a lead rested on a word another kind of address shares, and guessing would license a claim about the wrong set — pick one from considered), cursor_pins_ranking (you are paging a ranked list, so the address was not re-read mid-page). available lists the 20 addresses sharing the most words with your question (each relation's slots at most 10, slots_total counting them), with available_truncated saying whether any was left out and available_counts how many of each kind exist; limma_documents pages every table and its columns, and labels_offset every attribute and column name.
  • enumeration (dict, nullable) — THE CLOSED SET, present only for mode=enumerate. {coordinate, exist, delivered, truncated, bound} — exist is the count in the store BEFORE any page limit, which is the one number you cannot reconstruct from the rows you were given. bound: false means the address does not exist in this workspace's vocabulary, which is NOT the same as it existing and being empty: the first says nothing about your documents, the second says there are none of these. coverage.completeness_licensed states whether the pair licenses these are all of them, and coverage.withheld_because names why when it does not.
  • relations (dict, nullable) — Where the workspace merges duplicate records of the relation's entity type (an identity block on connect) this block also carries merging ({status, entity_type, unit, merged_entities, merged_records, proposals} — unit is record HERE: this verb counts and lists a relation's rows per RECORD, and a fact two merged records state is two rows), every row carries resolved_id (the entity its record belongs to), and a query with entity_id carries resolved: {resolved_id, members, merged_by, possible_duplicates} — ask each of members to read everything recorded about that entity; limma_search is the verb that counts merged entities. THE CLOSED SET FOR A relation_query, present only on that lane. {relation, query, where_run, expansion, unbound_slots, unknown_surface_slots, unplaced_slots, count, delivered, offset, truncated, bound, unbound_because, not_recorded, entities: {matched, bound, not_recorded}, undated, period_unjudged, absence, rows}. period_unjudged (null unless where names period_from / period_to) is {satisfied, undated, open_period}: rows matching every other clause whose named period column is EMPTY, which count leaves out. undated rows (no period_from) and open_period rows (an open end the clause needs past its witness as_of) cannot be judged; satisfied rows are open roles the record says DO satisfy (held at as_of 2024-03, so period_to gte 2020-01). Any of them withholds the licence (narrowed_by: undated_rows / open_period_rows / open_period_rows_not_counted); a row the record fails (period_to lte 2023-12 on that role) is in none, and withholds nothing. slot_unrecorded (null unless where names a declared slot) is {rows, slots: {slot: rows}}: rows that fail no clause on the record but hold NO value for a slot a clause constrains, which count leaves out without judging them — over rows that record no industry, industry eq X and industry ne X are both unanswerable, never both empty. Any withholds the licence (narrowed_by: slot_unrecorded_rows). query is what you asked; where_run is the where that RAN after every term clause was expanded through the workspace's vocabulary, and expansion prints each resolution (seniority: ≥ VP → {VP, SVP, C-level} via order). unbound_slots names term slots whose axis has no vocabulary, or whose ne / not_in negates an AMBIGUOUS surface: their equality clauses ran as written and the licence is withheld (narrowed_by: vocabulary_unbound:<slot>); a comparison on one makes the query unbound (unbound_because: vocabulary_unbound:<slot>). unknown_surface_slots names term slots where a value you asked is no term of the vocabulary ("England" where it lists United Kingdom): the value matched as written and the licence is withheld (narrowed_by: vocabulary_unknown_surface:<slot>) — a vocabulary cannot vouch that no row is what an unknown word names. unplaced_slots names term slots whose stored rows hold values no term places: those rows cannot be said to belong or not, so the licence is withheld (narrowed_by: vocabulary_unplaced:<slot>); a stored SURFACE of a term ("Vice President") is matched as its term and expansion says so. WHAT count LICENSES: it is the number of current rows matching the query in the store over your full visible scope, so count == delivered (at offset 0) with bound: true licenses these are all of them and count == 0 with bound: true licenses there are none. WHAT IT DOES NOT: bound: false means nobody looked — count is null, never 0, and not_recorded names the entities never looked at for this relation (per-entity: [entity_id]; workspace-wide: every known entity with no coverage row, and the count licenses nothing about them — coverage.withheld_because: narrowed says so). A KNOWN entity is one holding a row or a coverage row of some relation of the query's subject kind in your scope; per-entity, an id the address space holds nothing about — never loaded, outside your scope, or ERASED by a document delete — is unbound_because: entity_unknown with not_recorded: [], never listed as not recorded (entity_not_recorded is a known entity nobody looked at for this relation). ABSENCE AS A ROW: absence is the PER-ENTITY verdict {entity, none, not_recorded, split: {none, not_recorded, recorded}, truncated, licenses} (null when the query is unbound). Each verdict is {entity_id, kind, bound_by, denominator, coverage, corrected_rows}. kind: none — a COVERAGE ROW says source bound_by was read for this relation on this entity and zero rows of it are recorded (denominator 0; coverage lists every such row in your scope with declared_by — who ACTUALLY declared it: source for a document's own structure such as an export column, extractor for an extraction pass's claim — as_of and its receipt); it licenses no row of this relation is recorded for this entity in source S as of T, and nothing about sources that did not cover it. kind: not_recorded — no BINDING coverage row: no load declared it looked, or every source that did states a row of it that a correction retired (corrected_rows names those row ids; a source stating a corrected row is never the evidence for none), or a source states a row of it that its LOADER could not read (withheld: [{source_id, reason}], present only then: a quarantined value of a connected export — the licence then names withheld_rows, and such an entity is never none); bound_by and denominator are null and it licenses NOTHING about the relation for that entity: never read it as none, and never as clean. none is taken over rows of the RELATION, not rows matching where — an entity holding rows that do not match is recorded, not absent. With entity_id, absence.entity is that entity's verdict (null when it holds rows); workspace-wide, split counts every known entity and both lists are capped at 200 (truncated says so). licenses states both sentences on the wire. Each row: {handle, row_id, entity_id, relation, slots, period_from, period_to, as_of, sources, stated_by, source_id, provenance_kind, confidence, receipt, slot_receipts}. A row is ONE fact; sources lists every document IN YOUR SCOPE that states it — each {source_id, provenance_kind, confidence, receipt: {page, char_start, char_end, byte_verified}, slot_receipts: {slot: receipt}} — and stated_by counts them (2 = corroborated by two documents; a document you may not read is never listed). The flat source_id / receipt / slot_receipts / provenance_kind / confidence are the FIRST statement's. receipt is the span that states the row; byte_verified only when it was re-read byte for byte. With output_mode: compact the first statement in sources carries no slot_receipts of its own — they are the flat slot_receipts, sent once.
  • labels (dict, nullable) — THE NAMES THIS WORKSPACE FILES FACTS UNDER, over the documents YOU may see: {names, names_total, names_relevant, offset, truncated, next_offset, proposals_open, map_version}. Each name is {name, documents, attributes, aliases, records, type, proposals}: documents counts the documents (and connected sources) stating a current fact under it; attributes are the spellings to send as coordinate.attribute, records the connected relation.slot columns to send as relation_query. Spellings that differ only in case, punctuation, spacing or singular/plural are ONE name — a CSV column revenue and a PDF's "Revenue" — and nothing else is merged on its own: a fact's name is a name its document printed or a name a pack registered that its label equals, never a similar one. What a similarity match or a model suggested is a PROPOSAL, counted in proposals / proposals_open and never applied — until an operator ACCEPTS it (/v1/public/resolve, subject: names): the accepted spelling is then one of the name's aliases, its facts are delivered at name, and nothing stored changes (an undo takes it back out). A spelling the map re-spelled is an alias of its new spelling the same way. map_version is the map's version as you may see it: it moves when a name your documents carry is added, re-spelled or decided, never because of a document you may not see. Ranked by the words your question shares with each name, then by documents. BOUNDED, whatever the workspace holds: by default only the names sharing a word with your question are listed (at most 10; names_relevant counts them, names_total counts every name); send labels_offset / labels_limit to page the WHOLE map (next_offset continues it, null on the last page). A name lists at most 10 attributes / aliases / records, and <list>_total counts a longer list. type appears on a name only connected columns of that slot type hold (never a number).
  • elapsed_ms (number, default 0.0) — —

POST /v1/public/search — Search — a brief or constraints to the CLOSED SET of matching entities, with counts, receipts, None and Not recorded

Who may call it. Asks for read — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — matches rows the key's documents state. With criteria whose budget may spend (criteria_budget.max_reads above 0 — the default) it asks for write as well — then only: admin, analyst, service (anyone else: 403 criteria_read_needs_write). With an idea whose budget may spend (idea_budget.max_calls above 0 — the default) it asks for write as well — then only: admin, analyst, service (anyone else: 403 idea_grouping_needs_write).

Purpose

Enumerate the entities a request names, server side, over the relation space.

Refusals, each because the alternative is an answer to a different question: both or neither of brief / constraints (422; neither is a question only beside criteria); a constraint malformed for its slot's type (422); a brief with no model to compile it (503, reason names constraints); a compile that returned no tool call (502).

detail: "compact" answers the same search as the reply an agent holds — the matches, why each is in the set, who is one condition short (the close matches), counts of everything else, under keys that name no domain (a projection of the full reply: no number in it is computed differently); offset pages the matches and entity_ids restricts what is DELIVERED to named entities — how the evidence a compact reply left out is fetched. The default is the full warrant.

criteria adds ONE condition no relation can express, read by a model from the documents of the entities the constraints matched and joined to the partition as one more required group. Its refusals: two criteria, a malformed criterion, an unknown address or a budget above the deployment's limits (422); a read-only key asking for reads (403); an address that holds another schema (503).

idea asks about an IDEA instead of a thing ("where do we discuss pricing risk?"): the reply is the idea block — the document PARTS filed under labels that mean it, the parts under labels that possibly do, and completeness over the labelled parts with every part not labelled counted by cause (docs/integrator/ideas.md). Alone, or with constraints / brief / entity_ids narrowing the documents to those about the entities named. Its refusals: beside criteria, naming nothing, or a budget with no idea (422); a read-only key asking to sort labels (403).

Over MCP

limma_search(workspace_id: str = '', brief: Optional[str] = None, constraints: Optional[List[Dict[str, Any]]] = None, as_of: Optional[str] = None, limit: Optional[int] = None, criteria: Optional[List[Dict[str, Any]]] = None, criteria_budget: Optional[Dict[str, Any]] = None, list_limit: Optional[int] = None, offset: Optional[int] = None, entity_ids: Optional[List[str]] = None, detail: str = 'compact', idea: Optional[str] = None, idea_budget: Optional[Dict[str, int]] = None) -> Dict[str, Any]

Search: a request in plain language, or its constraints, to the CLOSED SET of matches.

idea — a question about an IDEA ("where do we discuss pricing risk?"): matches are then document PARTS (document, location, the quote — quote_at redeems with limma_region — and labels); possibly: parts that may discuss it — read them, never drop them. complete is over the labelled parts; incomplete_because counts parts not labelled and why (being_labelled: ask again). With constraints / entity_ids: their documents only. Read-only key: idea_budget={"max_calls": 0}.

detail is "compact" BY DEFAULT HERE (you re-read this reply on every turn; the HTTP verb's default is "full"). THE COMPACT REPLY: matched — total (the WHOLE set, whatever the page), showing (first and last rank), next_offset (send it as offset for the next page; limit defaults to 25). complete — true LICENSES "these are all of them", zero included; false: say why from incomplete_because (reason: count) and do not claim completeness. matches — {entity_id, name, why}: why is one line per condition, {condition, value, from, quote} (from: record — a row loaded as stated · text — read from a document, with its quote · computed; check appears only when the source no longer reads the value). name is the name the workspace recorded, null where it holds none — then name the entity by entity_id, never by a guess. about (also on close.matches) — up to 3 lines of what the workspace holds for the entity NOW: a role it holds, where it is; absent when it holds nothing. WHERE DUPLICATE RECORDS ARE MERGED (counted.unit names the entity type, e.g. person or company) each entry is ONE ENTITY — records says how many records were merged as it, on stated evidence, never on a name — and every count is of entities; counted.possible_duplicates counts pairs only PROPOSED as one entity (never merged; detail="full" lists them as possible_duplicates). counted.unit: record means a resolution is pending: a duplicated entity is counted twice. close — {total, showing, matches}, the CLOSE matches: a criterion's candidates with exactly ONE condition open (lacks): nothing stated for it (because: not_stated), or stated in a sentence nobody could verify (because: stated_not_verified, with that sentence as quote — read it) — worth a call, never a match; at most 10 listed. A candidate whose documents establish only a place (or a length) is not close: counted. candidates — beside a criterion, how many profiles its niche is read for. others — COUNTS only: ruled_out is the one "no"; cannot_tell (the documents say something else, or disagree), not_read and no_documents are never a "no". reading — present only while a criterion's read is unfinished (done of of, status, because). With background: true the rest is BEING READ WITHOUT A FURTHER CALL (retry_after_s: about how long it needs): call again with criteria=[{"address": <criterion>}] to WAIT for it — the call returns the moment the read is done and starts nothing twice. Without background, calling again is what reads on. spent — present only when a reply reports what reads cost (calls, reads, cost_usd). constraints — after a brief, the compile's reading: send it back in place of the brief. more — the calls for what was left out. more.entity, one entity's rows, quotes and receipts: this search with entity_ids=[…], detail="full" (with the criterion's address and criteria_budget={"max_reads": 0} it reads and spends nothing). The criterion's own working and NOBODY's rows (its facets, decomposition, estimate, stated_instead): the same with entity_ids=[]. map_version: as limma_facts. EVERYTHING BELOW DESCRIBES detail="full".

Use this for a BRIEF and read the set from the warrant, never from a loop of limma_facts calls. Pass EXACTLY ONE of brief (compiled by one model call) or constraints (that compiled form, sent directly: [{relation, slot, op, value, brief_span, required}], ops eq / ne / in / not_in / gte / lte). A deployment with no model answers a brief with 503 compile requires a model; pass constraints — do that. A workspace that declares NO relation answers a brief with 200 and no model call instead: bound false, unbound_because: workspace_declares_no_relation, every sentence uncompiled — nothing there can be searched. as_of is YYYY-MM; limit caps matches only (default 50; 25 compact) and offset skips that many. list_limit (0–200) caps every list of NON-matches — the None / Not recorded / Undetermined ids and a criterion's non-matching entries; counts stay whole. SEND IT (10; 0 for numbers only): unset, each list carries up to 200 ids.

What each block LICENSES:

  • matches — {entity_id, name, rank, rank_key, rows} in rank order. Every row carries provenance_kind, confidence and ONE of two receipt kinds: a row read from a DOCUMENT has receipt (source_id, char_start, char_end) — redeem it with limma_region; a row loaded from a DATABASE (limma_connect) has receipt null and per-slot slot_receipts naming db_table / row_key / db_column, re-read from that cell on delivery (byte_verified says whether it still holds the value). name is the display name the workspace recorded, null where it holds none: none is invented.
  • counts — over the WHOLE set, never the page: matched, none, not_recorded, undetermined, bound (= matched + none, membership determined), and per_constraint (each constraint ALONE). entities.matched lists the first 200 match ids in rank order (counts.matched is the total) — read it when truncated is true.
  • coverage.completeness_licensed — true LICENSES "every entity satisfying the request as run is in entities.matched", zero included ("there are none"). False names why in narrowed_by; do not claim completeness then.
  • none — per group, entities looked at for the relation with no satisfying row: LICENSES "no recorded row satisfies this condition" for them.
  • not_recorded — per group, entities NO load looked at for the relation. Licenses NOTHING: never report them as not matching, never as clean. Say "not recorded".
  • undetermined — entities whose only candidate rows cannot be judged: undated with no date that places them at as_of, or with an empty period column the record cannot decide a period_from / period_to constraint on (an open role witnessed at as_of 2024-03 satisfies period_to gte 2020-01 and fails period_to lte 2023-12; lte 2030-01 stays open), or holding NO value for a slot a condition names (groups[].unrecorded_slots: a role with no industry recorded is neither in Technology nor out of it). Neither match nor none.
  • gaps — what did not run: dropped (compiled constraints not tied to the brief's words, with reason), uncompiled (sentences no constraint came from: NOT searched), unbound (constraints the workspace cannot express). Tell the user each; any withholds the licence.
  • expansion — how the vocabulary read each term ("≥ VP → {VP, SVP, C-level}").
  • groups[].time_test — the rule as_of applied to each relation. groups[].reading: conditions on one relation are ONE row's. groups[].absence is limma_facts's per-entity verdict: absence.none entities hold NO row of the relation (coverage row the evidence, denominator 0).
  • provenance — {declared, extracted} over the delivered rows.
  • ranking — deterministic; no model ranks.
  • compiled — on the brief path, each constraint's brief_span and quote: check the reading; send corrected constraints if wrong.

criteria — ONE niche no constraint can express, READ by a model from documents: [{"text": "15+ years selling espresso machines to hotel chains in Southern Europe"}] (the WHOLE niche as one text, never one criterion per condition), [{"address": …}] to reuse one, or with your own split: "facets": [{"key": "market", "kind": "condition", "text": "in Southern Europe", "value": "Southern Europe"}, …] (text: the criterion's own words; kinds condition, duration + min_years — a MINIMUM, in years —, recency + current). Only entities your constraints matched are read (with no constraint: every entity of the kind); verdicts are stored, so asking again reads only new documents. criteria[0] partitions them: matched (evidence quotes; a condition checked_by vocabulary was not second-checked: read its quote; typing_call+second_call: a criterion-blind call named its term from those words), none (a quoted denial), undetermined (the documents do not say, or disagree — conflict: evidence holds both sides; a denial kept_as doubt, also under denials, is one the reader could not attribute to the person: quote it, it is no "no"), not_recorded (no document, or unread). Never report the last two as "no". counts and counts_detail (how many are one condition short, which condition is open for how many, why the unread are unread) cover EVERY candidate — take numbers from them, never from counting the listed entries. A match is a match for the facets ONLY: an entry's unchecked_words are words of your text nobody checked (nor were a facet's carried words — a lead-in such as "who sold"). status over_budget: nothing was read — add a constraint or raise criteria_budget (max_reads, max_usd, max_seconds — how long the CALL waits; the read it starts goes on without it, background; max_reads: 0 reuses only). partial: call again with the compiled constraints and the address (after read_failures, once). not_run: see status_reason — decomposition_incomplete: nothing was read — send the block's facets + suggested back as facets (yours to edit; of entries marked reading, ONE reading's: split or whole) — BUT where a piece of decomposition.incomplete says nothing_suggested (a negation, a bound, a length that is no minimum), REWORD the text instead: sent back as they are, the facets read the criterion WITHOUT those words and deliver the people it excludes (the words then ride every match as unchecked_words); ordinary_constraint_not_bound: fix the constraint gaps names; provider_refused: the model provider refused this call's reads (errors) — what earlier calls read still stands in counts, the rest is unread: say so, and do not loop. stated_instead lists, per condition, what the people ONE CONDITION SHORT state for it (states, people): where a value IS the thing asked for under another name (a kind of it, the trade's word for it — never a sibling or a broader category), send the criterion's facets again with that value in the facet's also ("also": ["espresso machines"]); such a match is checked_by accepted_name+second_call where the profile's sentence states the name as the person's own work in a form the reader knows, else second_call (the name stood as a vote and the second call decided). A name is matched as a profile STATES it, whole: a narrower or a longer phrase ("commercial espresso machines") is another name, yours to add. Where a relation RECORDS a person's roles, a criterion that asks for something CURRENT may carry it as record_check ({"relation", "party_slot"}): a stale "currently" is then caught — the entry says missing_because record_says_ended and closed_by_record (the row, its end): the profile is stale OR the record incomplete, say that you cannot tell which. Do NOT send it to get a length: reading a length from the role's dates is SWITCHED OFF — the block's record_check.length then says consulted: false (switched_off), nobody is matched on a record's dates, and a profile that states no length stays one condition short (duration_unknown): report it so. A LENGTH IS GRANTED only by one form of sentence: the person's given name (or full name; "I" only where the text is proved to be their own voice), then "has done this work for N years" (or "since ", "from to ") and nothing more, in plain running text, directly behind a sentence of that experience, and no longer than the experience's stated start allows. EVERY OTHER sentence the reading model reports as a length of an experience that establishes at least one condition — and every other sentence that points at such an experience's work ("this role", "that work") and writes a time — is HANDED ON, never read: missing_because says length_not_proved, and the entry lists it under lengths_not_proved — the whole sentence with its span, years (the count its words write; absent where nothing bounds it), bound, kept_as: "doubt" and why, closed words: not_in_the_closed_form, more_than_the_closed_form, behind_a_pronoun, name_not_proved, first_person_not_proved, subject_not_proved, subject_not_named, pointer_not_proved, not_running_text, contradicts_its_start, outside_the_experience, count_not_bounded, dates_not_usable, the_careers, of_an_earlier_time, organisation_not_proved. Quote it and say the length is stated but unverified — never "matched", never "no length". duration_unknown means what it says: no such sentence was reported or found (one that another person's full name leads, or that is about another organisation, is not handed on as theirs). In evidence, an item with denies: true is the person's own outright denial; one with kept_as: "doubt" may deny the condition and proves nothing; every other item supports. Judgments an EARLIER reader of this deployment stored are not served (reads.stored_not_served; the people are not_recorded, unread, cause read_by_another_reader): a search that may read reads them again, once. The text is visible to every key of the workspace: write the condition, not the client.

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • idea (string, nullable) — AN IDEA, not a thing: "where do we discuss pricing risk?". Answered over the PARTS of the documents in scope: matches are the parts filed under labels that mean it (document, location, the sentence, the labels), possibly the parts under labels that may. Each sentence's labels — short topics, each anchored in a word of its part — were written once at load; this call sorts the labels it has not sorted for this idea before (idea_budget) and stores the verdicts. complete is stated over the labelled parts; incomplete_because counts every part not labelled and why, and every sentence left without a topic (sentences_not_labelled). With constraints / brief / entity_ids, only the documents about those entities are in scope. Not with criteria (422). Sorting new labels needs a key with WRITE unless idea_budget.max_calls is 0. THE REPLY is not the entity warrant below: detail: full answers {workspace_id, idea: {text, key, grouper, labeller, complete, incomplete_because: {reason: count}, counts: {documents, parts, labelled, matched, possibly}, grouping: {labels, reused, judged_now, judged_twice, disagreed, not_grouped, verdicts}, matches, possibly, offset, limit, truncated, scope?, spent?}, elapsed_ms, compiled?} — each entry {source_id, document, entity_id, part, location, quote, quote_at, labels: [{label, broader, verdict, at}]}; detail: compact answers {detail, request_id, idea, matched, complete, incomplete_because?, matches, possibly: {total, showing, next_offset, matches}, counts, scope?, spent?, more}.
  • idea_budget ([SearchIdeaBudget](#searchideabudget), nullable) — The spend this call accepts for its idea.
  • brief (string, nullable) — The request in plain language. Compiled by ONE schema-constrained model call; send EXACTLY ONE of brief and constraints (422 otherwise). Needs a model: under a no-spend deployment this is a 503 naming constraints as the way in. A workspace that declares NO relation has nothing to compile to: a 200 with no model call, unbound_because: workspace_declares_no_relation and every sentence uncompiled.
  • constraints (list of [SearchConstraintSpec](#searchconstraintspec), nullable) — The compiled form, sent directly — no model call. A condition on a relation the pack does not declare is unbound; a niche no relation can express goes in criteria.
  • criteria (list of [SearchCriterionSpec](#searchcriterionspec), nullable) — ONE niche no relation can express, READ by a model from the documents of the entities the constraints matched (with no constraint: every entity of the subject kind), and nobody else's. A niche is ONE criterion — its conditions must hold of one experience — so send it whole, never one criterion per condition. A MATCH IS A MATCH FOR ITS facets ONLY: words of the text no facet took are returned as unchecked_words. Every judgment is stored with its quotes and lives as long as its DOCUMENT is current, so the same criterion again reads only new documents (a document sent again as a new source does not retire the old one's judgment). When a required constraint does not run (an unknown relation; one the compile dropped) nobody is read: ordinary_constraint_not_bound. May be sent with brief, with constraints, or alone. One per search (422 too_many_criteria). Reading costs model calls: it needs a key with WRITE unless criteria_budget.max_reads is 0.
  • criteria_budget ([SearchCriteriaBudget](#searchcriteriabudget), nullable) — The spend this call accepts for its criterion.
  • as_of (string, nullable) — YYYY-MM. A dated row counts when its half-open period contains the month; an undated row when it is current and the relation's one date slot is on or before the month (groups[].time_test states the rule per group). Wins over a month the brief pinned (as_of_source). Omitted: every current row counts.
  • limit (integer, default 50) — Matches on this page, in rank order.
  • list_limit (integer, nullable) — How many entities each list of NON-matches carries: the None / Not recorded / Undetermined ids (entities, and per group), a group's absence entries, and a criterion's none / undetermined / not_recorded entries (nearest misses first). Omitted: 200, the cap. Every COUNT is of the whole set whatever this is (counts, each group's total, absence.split, a criterion's counts and counts_detail), so a list shorter than its count was cut — and an agent that reports only numbers can send 0. Matches are never cut by it: entities.matched and a criterion's matched keep their cap.
  • offset (integer, default 0) — Matches to skip, in rank order: the page is ranks offset + 1 … offset + limit. truncated says whether more follow.
  • entity_ids (list of string, nullable) — DELIVER ONLY THESE ENTITIES — how the evidence of entities an earlier reply only named is fetched (a compact reply's more.entity). matches then holds those of them that match (each with its true rank, every row and receipt), and a criterion's four lists hold their entries only (the quotes, the reasons). Nothing else changes: every count is still of the whole set, and nobody is read who would not have been. Sent with a criterion's address and criteria_budget: {"max_reads": 0} it serves STORED judgments and spends nothing.
  • detail (one offull,compact`, default'full') —full(the default here): the whole warrant below.compact: the reply an AGENT holds — the matches, one line per condition on why each is in the set, who is one condition short, and COUNTS of everything else; a projection of the full reply (no number in it is computed differently), sized by the matches returned and never by the candidates examined. No key of it names a domain: an entry is a match, whatever kind of entity the workspace holds. Its shape:}, complete, incomplete_because?, as_of?, criterion?, candidates?, constraints?, matches, close, counted?, others, reading?, spent?, more.matched.totaliscounts.matched; page withoffset(next_offset, null on the last page);limitdefaults to 25 here.completeiscoverage.completeness_licensed, andincomplete_becauseat most five of its reasons asreason: count.matches[]:—records: present when duplicate records were merged into this one entity, how many;about: at most 3 lines (120 characters each) of what the workspace's own relations hold for the entity NOW, whatever was asked — the row of each relation it holds most recently (a role held, dated or not, then a standing fact such as a place, then a role that ended), a term said by its vocabulary's own label where the vocabulary holds it, in the caller's document scope; absent when it holds none.whyone line per condition established:{condition(the constraint asrelation.slot op value, or the criterion's own words for a facet),value(what the row or the document states),from(record: a row loaded as stated ·text: read from a document ·computed: a length taken from a record's dates),quote?(the supporting words, at most 160 characters, fortext, where they say more thanvalue),check?(present only when the source no longer reads the value: why)}. close: {total, showing, matches} — the close matches: the criterion's candidates with EXACTLY ONE condition open; each {entity_id, name, lacks (the open condition), because (not_stated: their documents say NOTHING for it · stated_not_verified: a document states it in a sentence the reader did not take as proved), quote? (for stated_not_verified: that one sentence, at most 160 characters), why (the conditions their documents do establish)}andabout?as above, nearest first, at most 10 listed. A candidate whose documents state something ELSE for the open condition, or contradict each other, or establish no condition of substance (only a place, a length or a time), is NOT close: it is counted inothers.cannot_tell.counted: present where the workspace merges duplicate records —: whatmatched.totalcounts (unit: the resolved entity type, e.g.personorcompany, once resolution is complete;recordwhile it is pending) and how many pairs touching the answer are only proposed as one entity.others: counts only —ruled_out(counts.none),cannot_tell(counts.undeterminedless the close),no_documents(candidates with no document on file) andnot_read(the rest ofcounts.not_recorded): never report the last three as a no.reading: present only while a criterion's read is unfinished —; call again withcriterionas the criteriaaddress(a criterion is read only while a call is open: the reads still running when one returns finish behind it, and nothing new starts until the next call).spent: present only when THIS call made model calls for the criterion —, the block's own usage (cost_usdnull: could not be priced, never free); a call served from stored judgments carries none.criterion: that address;candidates: beside it, how many profiles its niche is read for — everybody the ordinary conditions leave (criteria[0].candidates.entities), which is what THIS question checks whereotherspartitions the whole directory.constraints: on thebriefpath, the compile's reading — send it back in place of the brief (a brief is compiled again on every call).more: the calls that return what was left out —entity: this search withentity_idsanddetail: "full"(those entities' rows, quotes and receipts; with the criterion's address andmax_reads: 0it reads and spends nothing), the same withentity_ids: [](criterion, when the search has one: NOBODY's rows — the full reply's criteria block with empty lists: its facets, decomposition, estimate,stated_instead,unchecked_words,counts_detail),rows:factsfor one entity's rows of a relation,receipt:regionfor a receipt.list_limitis not read: a compact reply lists no non-match.request_idis this call's id — its audit event (/v1/audit/events`) records the facts and spans the answer rests on and what the criterion spent.

Response

The search warrant. Every block below states what it LICENSES.

  • workspace_id (string, required) — —
  • bound (boolean, required) — false when no REQUIRED constraint could run (unbound_because): nothing was enumerated, every count is null, and an empty matches means nothing.
  • unbound_because (string, nullable) — —
  • as_of (string, nullable) — —
  • as_of_source (one ofrequest,compiled, nullable) — Where as_of came from: the request, or a month the brief pinned (compiled.as_of).
  • limit (integer, required) — —
  • list_limit (integer, nullable) — PRESENT ONLY when the request sent it: the bound it put on every list of non-matches. Absent, those lists are capped at 200.
  • offset (integer, nullable) — PRESENT ONLY when the request sent it: the matches skipped before this page.
  • entity_ids (list of string, nullable) — PRESENT ONLY when the request sent it: the entities this reply's matches and criterion lists were restricted to. Every count is still of the whole set.
  • truncated (boolean, required) — More entities match than this page carries; entities.matched lists the first 200 in rank order and counts.matched is the total.
  • ranking (string, required) — Always deterministic: optional-groups, threshold-distance, entity_id — optional groups satisfied (desc), then each gte/lte constraint's distance from its threshold (asc, in request order; matches[].rank_key), then entity_id. No model.
  • matches (list of dict, required) — {entity_id, name, rank, rank_key, rows}. rows are EVERY current row that satisfied a group for this entity at as_of, each {row_id, handle, relation, slots, period_from, period_to, receipt, slot_receipts, provenance_kind, confidence, sources, stated_by, satisfies_groups}. A row read from a DOCUMENT has a receipt (source_id, char_start, char_end) that redeems through /v1/public/region; a row loaded from a DATABASE (connect) has receipt null and per-slot slot_receipts naming db_table / row_key / db_column, re-read from that cell on delivery (byte_verified). name is the display name the workspace RECORDED for the entity — what a document load's name_path wrote beside the documents it registered, or, for an entity no document names, read from its record — and null where it holds none, or two that differ: a name is never derived from an id or read out of a text. An entry of a criterion's lists carries the same name when one is held. WHERE RECORDS ARE MERGED (merging.unit is the entity type) A MATCH IS ONE THING: entity_id is the merged entity's id (one of its records' ids — every member id stays a valid handle), name its display name (its own record's, else the first member's that states one), and the match also carries members ([{entity_id, name}], the records merged as it; a record that is its own entity has itself), merged_by ([{entity_ids, kind, decided_by, evidence, vetoes}] — what each merge rests on: a shared identifier is attested key_equality_at_load and never shown as a value; [] for a single record) and possible_duplicates (ids of entities only PROPOSED as the same thing — never merged, never counted as one). Each row keeps the entity_id of the record that states it, and the rows of a merged entity carry same_fact_as: the other records' rows stating the SAME fact — one role stated twice is one role.
  • counts (dict, required) — {universe, matched, none, not_recorded, undetermined, bound, per_constraint} over the WHOLE set. matched + none is bound (membership determined). per_constraint[i] counts constraint i ALONE: {status, matched, rows, bound, none, not_recorded, undetermined} — status unbound rows carry nulls. Where merging.unit is the entity type (merges applied) every count here is of merged ENTITIES of that type, and three keys are added: records ({universe, matched, none, not_recorded, undetermined} — the same partition counted per record), matched_lower_bound (entities matched if every proposal among the matches were confirmed) and matched_upper_bound (if every unlisted group of possible duplicates that could together match were one more entity; on a licensed answer it equals matched). Neither bound knows a duplicate nothing proposed.
  • possible_duplicates (list of dict, nullable) — Present where records are merged: the PROPOSALS that touch this answer — {entity_ids: [a, b], resolved_ids: [entity of a, entity of b], kind, names, statuses: {resolved id: matched \| undetermined \| none \| not_recorded}, evidence, vetoes} for every pair somebody has not confirmed in which one side matched or is open. NOTHING HERE IS MERGED: each side is counted as its own entity. kind says how weak the evidence is (same_name, one_shared_fact, shared_facts_undated, common_facts_only, identifier_unconfirmed, identifier_names_differ, conflicting_evidence, cluster_conflict, cluster_too_large). A record proposed as the duplicate of a matched or open entity never reads none (undetermined, reason possible_duplicate). Bounded by list_limit like every list of non-matches; merging.possible_duplicates_total is whole. Confirm or reject a pair with resolve (its entity_type: merging.entity_type).
  • merging (dict, nullable) — Present once an identity block was stated for the entity type this search counts (connect): {status, entity_type, unit, run_id, resolved_at, merged_entities, merged_records, proposals, possible_duplicates_total, none_withheld_by_proposals} over what this caller may read. unit is the ENTITY TYPE (person, company, …) when status is complete: this reply counts merged entities of it. While a resolution is pending, running or failed — or the type's blocks are in config_conflict — unit is record: one record is counted as one entity (a duplicate is counted twice; nothing is merged) and the merge fields of matches and counts are absent.
  • provenance (dict of integer, required) — {declared, extracted} over the rows DELIVERED in matches: declared rows were loaded as stated; extracted rows were read by a model and carry its confidence.
  • map_version (integer, nullable) — The workspace map's version as you may see it — the same number as limma_facts' labels.map_version: it moves when a name your documents carry is added, re-spelled or decided, never because of a document you may not see.
  • entities (dict of list of string, required) — The partition's ids (capped at 200; totals in counts): matched (rank order), none (determined NOT to match — a required group looked at the entity and no row satisfies it), not_recorded (some required relation was never looked at for the entity: NOT evidence it fails), undetermined (only rows that cannot be placed at as_of, or that hold no value for a constrained slot).
  • groups (list of dict, required) — One per (relation, required): the clauses RUN (where_run), time_test (the rule applied at as_of), the row tallies, and the per-group partition counts. Conditions on one relation are ONE row's conditions, and reading says so per group. unrecorded_slots names each constrained slot a candidate row holds NO value for: such a row fails no condition and satisfies none — its entity is undetermined unless another row matches (undetermined_reasons: slot_unrecorded). absence is the per-entity verdict of limma_facts: absence.none names the entities holding NO row of the relation at all, each with the coverage row that says a load looked (denominator 0); absence.not_recorded the entities never asked.
  • none (list of dict, required) — Per group: {group, relation, required, total, entity_ids} — entities looked at for the relation with no satisfying row. LICENSES 'no recorded row satisfies this group' for them.
  • not_recorded (list of dict, required) — Per group, same shape: entities no load looked at for the relation. LICENSES NOTHING about them — never read as none.
  • undetermined (list of dict, required) — Per group, same shape: entities whose only candidate rows cannot be judged — undated with no date that places them at as_of, or with an empty period column the record cannot decide a period_from / period_to constraint on (an open role held at as_of 2024-03 SATISFIES period_to gte 2020-01 and FAILS period_to lte 2023-12; only lte 2030-01 is open), or with no value recorded for a constrained slot.
  • gaps (dict, required) — What of the request did NOT run: dropped (compiled constraints not attributable to whole words of the brief, or that named no address the tool offered — each with reason), uncompiled (sentences of the brief no constraint came from), unbound (constraints the workspace cannot express — the search ran over the rest). Any entry withholds the licence. OFF BY DEFAULT (a deployment switch, LIMMA_SEARCH_INEXPRESSIBLE=1): on the brief path the compile may also list inexpressible — conditions the brief states that NO relation can express, each {quote, brief_span} in the brief's own words. That part of the request was not searched (brief_inexpressible withholds the licence) and nothing is read automatically. A niche is ONE criterion: send the brief's niche — the words from the first to the last listed quote, or your own sentence joining them — as ONE criteria text, never one criterion per quote (each quote read alone checks one condition of anybody's any experience).
  • expansion (list of string, required) — The vocabulary's printed resolution of every term constraint (seniority: ≥ VP → {VP, SVP, C-level} via order).
  • coverage (dict, required) — {completeness_licensed, withheld_because?, narrowed_by?} — the ONE completeness rule. Licensed: every entity satisfying the request as run is in entities.matched. narrowed_by names each reason it is not (acl, constraints_dropped, brief_uncompiled, constraint_unbound:<i>, entities_not_recorded, relation_not_recorded:<r>, undated_rows_unanswerable:<r>, open_period_rows_unanswerable:<r>, corrected_rows_unanswerable:<r>, withheld_rows_unanswerable:<r> (a connected export states a row for the entity that its loader could not read, or reads from a second place that says nothing for the entity: it is undetermined, never none), unattributed_rows_withheld:<r> (an export holds a document nobody could tie to an entity, so no list can be complete), slot_unrecorded_rows_unanswerable:<r>, brief_inexpressible, and a criterion's criterion_not_read:<crit>, criterion_silent:<crit>, criterion_conflict:<crit>, criterion_partially_decomposed:<crit>, criterion_documents_outside_universe:<crit>).
  • compiled (dict, nullable) — The compile, on the brief path: {brief, constraints (each with brief_span and quote), dropped, uncompiled, as_of, model, usage} — and inexpressible where the deployment switched that output on (see gaps). Null on the constraints path.
  • criteria (list of dict, nullable) — PRESENT ONLY when the request carried criteria: one block per criterion. address (send it back to reuse the criterion exactly), minted (this call created it), text, facets (each typed or why not; core: what the reader's own rules compare with when the quote opens with a lead-in — "repaired espresso machines" → "espresso machines"; carried: the lead-in words before the quote that the facet carries — "who repaired"; completed_by: reader: a condition the reader added from words a model's decomposition left out and the vocabulary types), decomposition (what became of the split THIS call made: accepted with the reader's actions, or incomplete with each piece it refuses and why; null for a stored criterion or your own facets), unattributed (words of the text no facet took) and unchecked_words — those of them that are more than framing ("experts with"): words that may state a condition and that NOBODY CHECKED. A match is a match for facets only; unchecked_words withhold the licence and are repeated on every matched entry. record_check — null unless the criterion states one: the check as stated, and WHAT IT WAS READ FOR — open_period {consulted: true} (a "currently" or "since …" at an employer whose every recorded role has ended establishes nothing current) and length {consulted, why}: false with why switched_off — the step that reads a length the profile does not state from the role's dates is off in the product, and no request turns it on (or no_duration_facet: the criterion asks for no length). With length.consulted false nobody is matched on a record's dates. (A criterion NOT RUN echoes the check the request sent, consulted for nothing: open_period.why is not_run.) status: complete (every candidate document holds a judgment or a recorded failure) · partial (status_reason time_budget: the call returned before the read was done — background.reading true says the rest is BEING READ WITHOUT A FURTHER CALL: call again with the address to wait for it, the call returns the moment it is done; spend_cap, model_unavailable: call again with the address; read_failures: reads.failed says which by cause — a call again retries them, and a document that fails twice will fail again; max_seconds_below_read_p95: no read fits this max_seconds, raise it) · over_budget (NOTHING was read: estimate says what it would cost, unconstrained_relations what else could narrow it) · over_budget_during_read · not_run (status_reason: model_unavailable, reuse_only_criterion_unknown, decomposition_failed, decomposition_timeout, provider_refused; decomposition_incomplete — the reader could not prove the model's split covers the text (a piece that is more than framing is in no facet, a condition holds a number or a word of time, or a condition holds a vocabulary term beside other words without being typed by it), so NOTHING was minted, read or stored: facets holds what does hold, suggested each leftover piece as the facet the reader would add (with why), unattributed the words left — send facets + suggested back as your own facets, as they are or edited. A piece that holds a term beside other words is suggested TWO ways, each entry marked reading: split (the term as a condition the vocabulary judges, and the rest) or whole (one condition, judged by the model's vote) — send ONE reading, both at once is a 422; the same text sent again is decomposed again; ordinary_constraint_not_bound — a required constraint did not run (gaps.unbound / gaps.dropped), so who to read is unknown: nobody was read and no stored verdict is served, fix the constraint; no_candidates_in_relation_space — the documents are about entities no relation knows). candidates (who was eligible to be read, and from where), reads (needed = reused + to_read; done, failed by cause, failed_on_record — documents on record as unread, by cause: an earlier read failed for good, or its judgment was corrected; stored_not_served — stored judgments this search did NOT serve, by cause: read_by_another_reader — a judgment is reused only by the reader that could have written it (its version and the setting of every one of its switches: the block's reader_identity), so after an upgrade or a changed switch each stored judgment is read again once, and a reuse-only search (max_reads: 0) lists those people not_recorded, unread, with that cause; not_submitted, in_flight_at_return; adjudicated: reads that made the reader's SECOND call — a document every check leaves a match is put to one more narrow call and matches only if that confirms what it was asked; WHAT it was asked of each condition is that condition's checked_by; typed: reads that made the reader's TYPING call before it), background (what is read WITHOUT A FURTHER CALL: reading — a background reader holds documents of this search; started — the reads THIS call handed it, read to the end under this call's budget; queued / in_flight — of this search's documents, at return; retry_after_s — about how long it needs for them: a call made sooner waits for them itself, up to its max_seconds; a second call never starts a document a reader holds), usage (the provider's tokens over every call of the reads of this criterion that LANDED and that no earlier response reported — those this call watched land and those that landed in the background between calls; said once, to a call that may spend; one still in flight at return is in the next such response) and estimate (tokens, cost_usd, cost_basis — an UPPER BOUND: it prices that second call for every read, estimate.adjudication — and the typing call, estimate.typing; calls_to_complete: how many calls, each waiting its max_seconds, until the read is done), budget. THE PARTITION OF THE CANDIDATES, each list capped at 200 (the three lists of non-matches at the request's list_limit) with totals in counts — and counts_detail, over EVERY candidate whatever was listed: undetermined.one_condition_short (exactly one facet OPEN by the reader's own account — missing_because — a length the profile states is listed missing beside its conditions and is not open), .conflict, .missing (per facet: the undetermined candidates for whom it is open), .missing_because (the same people per facet BY THE READER'S REASON — duration: {length_not_proved: n, duration_unknown: m} tells a profile that states a length the reader did not take from one that states none) and .lengths_not_proved (how many undetermined candidates carry a handed-on sentence); not_recorded per reason (a reason nobody has is ABSENT: read a missing no_documents or unread as 0) and .unread_causes — and stated_instead: per condition, what the candidates ONE CONDITION SHORT state for the condition they lack (values: states and people, most first, capped at list_limit; distinct: how many there are) — only words that could be sent back as a name (never a value the vocabulary places outside a typed condition, a denial, a sentence, a name the criterion already holds). Words, not judgments: where one of them IS the thing your condition asks for under another name, send the criterion again with that name in the facet's also: matched (every document of the entity was read; one states every condition of ONE experience with verified quotes; none contradicts — evidence lists each facet's quote with its span, states: the words in it that name the value, and for a condition checked_by, who confirmed it: second_call (one more narrow model call, asked whether that quote states the condition of the subject's own work, said yes) · vocabulary+second_call (the workspace vocabulary placed the stated value under the condition's term, and the second call, asked whose work the quote describes, said the subject's own) · vocabulary alone (the vocabulary placed the value and nobody was asked: it says the VALUE fits, not whose work the sentence describes — read the quote before relying on it) · typing_call+second_call / vocabulary+typing_call+second_call (the words first offered were ones the vocabulary cannot place — a job title — so one more model call, shown that experience's own sentences and the vocabulary's terms and NOT the criterion, named the term the sentences state; the quote is the words it pointed at and term the term it named — and the second call, asked whose work those words describe, said the subject's own; without +second_call nobody was asked) · accepted_name+second_call (the stated words write a name YOU accepted for the condition — a facet's also — given as accepted_name, and the second call, asked whose work they describe, said the subject's own; accepted_name alone: nobody was asked); a LENGTH is never checked_by record in this deployment: the step that reads one from the dates of the record's own role(s) — years_recorded, record_rows, record_periods on the evidence, rests_on_record on the entry — is switched off, and the block's record_check says so) · none (a verified statement that the entity did NOT meet a condition, and no support) · undetermined (silent: the documents do not say, with established / missing — listed nearest first (most conditions established), each with missing_because — per missing facet, why, in the reader's words: silent, different (the document states another value: see different), second_call:different / second_call:others / second_call:cannot_tell (see adjudication; second_call:role_different: the value was settled and the second call read its words in another role), or the reason its evidence was refused (quote_not_in_document, duration_below_threshold, …) — and record_says_ended for a time facet the record_check withheld: the profile writes the role open ("since 2001", "currently") and every role the record holds at that employer has ended; the entry then carries closed_by_record {row, end}, the record row and its end month. It is a stale profile OR an incomplete record — the reply cannot tell which — and length_not_proved for a duration: a document STATES a length and the reader did not take it (see lengths_not_proved); conflict: they say both — evidence then lists the supporting quotes and each denial; a FIRM denial is marked denies: true, and a denial marked kept_as: "doubt" is a sentence of the person's own documents that may deny the condition and that the reader could neither prove to be the person's outright denial nor prove to be somebody else's ("The firm has 400 staff. She has never worked on …"): it makes a match a conflict and is never a none by itself) · not_recorded (no_documents, no_text, or unread with its causes — never a no; corrected: a judgment of that document was corrected, so it no longer speaks). An entry of any list may also carry denials — each denial kept as a doubt, the quote with its span — and lengths_not_proved — per LENGTH a document states that the reader did not prove this experience's (the last paragraph of this description says what is granted, what is handed on and with which why) — and different — per condition a document states ANOTHER value for, the quote with its span and states (why a near-miss is one; never a no) — and adjudication — per document the second call judged, outcome confirmed (of what it was ASKED: see checked_by) or disagreed, with not_confirmed: each condition it doubted, its answer (different | others — the words describe someone else's work | cannot_tell) and the quote. A document it disagreed on is undetermined, never a match. A document that STILL matches may carry not_heeded: an answer of the second call that was not a yes and that the reader did not act on (different about a value the vocabulary had settled) — two checkers disagreed about a condition the match rests on, and its checked_by is then vocabulary alone. The block's adjudication counts them (documents, confirmed, disagreed, not_heeded). APART from it, typing — per condition of a document that criterion-blind call was asked about: its answer (yes | different | others | cannot_tell), the outcome (established | different | not_confirmed | refused with reason), the term judged and the words it pointed at; the block's typing counts them (missing_because then reads different, typing_not_confirmed, typed_on_a_title, words_name_another_axis — a title alone establishes nothing). A document it left no match was never put to the second call. The criterion is also one more REQUIRED group of groups (groups[].criterion), so entities, counts and matches[].rows (the criterion's supporting rows, each receipt redeemable through /v1/public/region) already include it; coverage.narrowed_by names criterion_not_read:<address>, criterion_silent:<address>, criterion_conflict:<address>, criterion_partially_decomposed:<address> and criterion_documents_outside_universe:<address>. A LENGTH IS GRANTED only by one form of sentence: the person's given name (or full name; "I" only where the text is proved to be their own voice), then "has done this work for N years" (or "since ", "from to ") and nothing more, in plain running text, directly behind a sentence of that experience, and no longer than the experience's stated start allows. EVERY OTHER sentence the reading model reports as a length of an experience that establishes at least one condition — and every other sentence that points at such an experience's work ("this role", "that work") and writes a time — is HANDED ON, never read: missing_because says length_not_proved, and the entry lists it under lengths_not_proved — the whole sentence with its span, years (the count its words write; absent where nothing bounds it), bound, kept_as: "doubt" and why, closed words: not_in_the_closed_form, more_than_the_closed_form, behind_a_pronoun, name_not_proved, first_person_not_proved, subject_not_proved, subject_not_named, pointer_not_proved, not_running_text, contradicts_its_start, outside_the_experience, count_not_bounded, dates_not_usable, the_careers, of_an_earlier_time, organisation_not_proved. Quote it and say the length is stated but unverified — never "matched", never "no length". duration_unknown means what it says: no such sentence was reported or found (one that another person's full name leads, or that is about another organisation, is not handed on as theirs). In evidence, an item with denies: true is the person's own outright denial; one with kept_as: "doubt" may deny the condition and proves nothing; every other item supports.
  • elapsed_ms (number, default 0.0) — —

limma_plan

POST /v1/public/plan — Compile a question WITHOUT executing it — a dry run

Who may call it. Asks for read — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — compiles the question; opens no document.

Purpose

Show what the engine WOULD do, before it does it.

Over MCP

limma_plan(question: str, workspace_id: str = '') -> Dict[str, Any]

Show what Limma WOULD do with a question, without executing it. A dry run.

Call this BEFORE limma_ask when the question is unusual or the cost of a wrong answer is high. It compiles only, so it is cheap, and it free-rides the compile cache — a plan followed by an ask for the same question costs ONE compile, not two.

Read three fields in this order:

  • capability_gap — non-null means the engine cannot EXPRESS your question at all. An ask would come back refused and you would not be able to tell whether the fact was missing or the question was unaskable. Those need opposite responses: rephrase, versus supply more documents.
  • interpretation — the compiler's own reading of your question, in prose. If it misread you, fix the question here rather than paying to execute a misreading.
  • steps — the operations that would run. These are the same steps the answer actually executes.

Also returns answer_form (value / yesno / name / set), sources_bound, and estimated_cost.cached — true when this compile cost no LLM round-trip.

Request

  • workspace_id (string, nullable) — The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • question (string, required) — —

Response

  • workspace_id (string, required) — —
  • question (string, required) — —
  • answer_form (string, nullable) — value | yesno | name | set — the SHAPE of answer the compiler read the question as wanting.
  • interpretation (string, nullable) — The compiler's own reading of the question, in prose. If this is wrong, the answer will be too — rephrase and re-plan rather than paying to execute a misread question.
  • steps (list of [PlanStepOut](#planstepout)) — The operations that WOULD run, in order. These are the same steps the subsequent answer executes.
  • capability_gap (string, nullable) — THE POINT OF THIS VERB. Non-null when the engine cannot express this question in its plan algebra at all — an answer would come back REFUSED and you would not know whether the fact was missing or the QUESTION was unaskable. Those need opposite responses.
  • grammar_error (string, nullable) — The compiler produced no valid plan (distinct from an explicit capability gap: here it failed to parse rather than declaring the question inexpressible).
  • sources_bound (list of string) — The documents an answer would draw on. One per workspace today.
  • estimated_cost (dict) — {compile_rounds, cached} — LLM rounds this compile spent. cached=true means it cost nothing and the answer will reuse it.
  • elapsed_ms (number, default 0.0) — —

upload (REST only)

POST /v1/public/upload — Upload ONE file through an upload link (REST — the link comes from limma_ingest with upload_link: true)

Who may call it. Takes an upload link's token (X-Limma-Upload-Token), never a key. The upload is made as the key that minted the link, judged when the bytes arrive by the document door's rule: it must hold write (roles: admin, analyst, service), reach the link's workspace by its organization and scope, and be confined to no document — anything else is 403 upload_link_key_refused.

Purpose

The bytes of ONE file, sent with an upload link's token — the body IS the file (Content-Type any but multipart/*; curl -X POST -T report.pdf).

The link (limma_ingest with upload_link: true) names the workspace, the file's name and what it is read under, and the key that minted it. That key is judged again as the bytes arrive, by the document door's rule; the file is then queued EXACTLY as POST /v1/workspaces/{workspace_id}/documents queues one (the same function), and the reply is that door's entry for it.

A LINK IS SPENT by the first upload it admits — whatever then happens to the bytes — and expires 15 minutes after it was minted: send the file once, and mint another link to send it again. 401 without a token or with one no link has; 410 for a link spent or expired; 400 when the request asserts principals or presents an API key other than the one that minted the link; 403 when that key may no longer upload (revoked, re-scoped, read only); 404 when its workspace is gone; 413 over the link's max_bytes; 415 for a multipart body; 422 for an empty one.

No MCP tool

This verb has no MCP tool because it carries a FILE's bytes, which no tool call can: limma_ingest with upload_link: true mints the one-time link, and the agent's shell — or the person it hands the link's command to — sends the file to this door with it.

Request

  • wait_s (number, nullable, query) — Wait up to this many seconds (0-120) for the file to be read; the reply's wait says why it returned. Left out, the call returns once the file is queued, as the document door does.

Response

  • workspace_id (string, required) — —
  • documents (list of dict, required) — One entry, for the file: exactly the entry POST /v1/workspaces/{workspace_id}/documents answers for a file it is sent (document_id, processing_status, status, ingest_reason, job_id, sync, ...), its status read again after the wait.
  • wait (dict, nullable) — Present when the request asked for wait_s: the wait block of POST /v1/public/ingest — {outcome: read | still_reading | erased | no_reader, wait_s, waited_s, held: {facts, records}, reading, failure: {reason}, erased, erased_at}.

upload_export (REST only)

POST /v1/public/upload_export — Upload an export file for connect (multipart; REST and the SDK only — the agent connects it by export_id)

Who may call it. Asks for write — roles: admin, analyst, service. A key scoped to workspaces reaches only those. A key confined to documents is refused: 403 document_scoped_key_refused.

Purpose

Store an EXPORT FILE for a later connect and answer its export_id.

For a caller that cannot put a file on the API host — a deployment it does not run, or one of several API processes that share no disk. The file goes to this deployment's SOURCE BACKEND (LIMMA_SOURCE_BACKEND: a bucket or a directory every API process reads, or one process's memory — retention says which), under an id the server mints, in the workspace's own space. connect then names it as document_source: {"export_id": ..., "collection": ...} instead of a path, and reads it with the same reader a path takes.

One multipart request: workspace_id and the file as file, under a name that states its format (any other name is 422 export_name_refused: an upload is never read as JSON by default, as a path is). At most EPISTEM_CONNECT_MAX_EXPORT_BYTES (512 MiB unless the operator set another; 413 over it). A writer's key on the workspace, as for connect: a read-only key and a key confined to documents are refused 403.

NO ROUTE READS THE BYTES BACK. Only a connect of this workspace reads them, from a private temporary copy it removes when it ends; a pull that writes its generation deletes the export (a proposal and a refused pull keep it for the next call). There is no MCP tool: a file's bytes do not belong in a tool call's arguments — the integrator uploads over HTTP or with the SDK (upload_export), and the agent connects the id.

No MCP tool

This verb has no MCP tool because it carries a FILE, and a file's bytes do not belong in a tool call's arguments: the integrator uploads the export over HTTP (multipart) or with the SDK (upload_export), and the agent passes the export_id it answers to limma_connect as document_source.export_id.

Request

  • workspace_id (string, nullable) — The workspace the export is for: only a connect of this workspace reads it. The workspace. Left out (or null), the call is served from the calling key's default workspace, created on first use by a key that may create workspaces; the reply's workspace_id names the one served.
  • file (string (binary), required) — The export file, under a name that states its format: .csv, .tsv / .tab, .xlsx / .xlsm, or .json / .jsonl / .ndjson (a mongoexport file).

Response

An export file stored for THIS workspace's connects, and how long it is held.

  • workspace_id (string, required) — The workspace the export is held for: a connect of any other workspace answers its id 404, as it answers an id never minted.
  • export_id (string, required) — Pass it as document_source.export_id to POST /v1/public/connect (MCP limma_connect) in this workspace — mode: propose first, then the pull. Minted by the server (exp_, 32 random hex digits, the file's suffix); only a connect of this workspace can read it, and no route returns the bytes.
  • bytes (integer, required) — The size of the file stored, in bytes.
  • sha256 (string, required) — SHA-256 of the bytes stored, as hex: compare it with the file you sent.
  • filename (string, required) — The uploaded file's name as taken (its last path component). The export is known by export_id and, once connected, by its collection; the name is kept nowhere but its suffix.
  • format (string, required) — The format the name states — json (one document per line or a JSON array), csv, tsv or xlsx — which a connect reads it in, unless its document_source.format states another.
  • expires_at (string, nullable) — When the source backend will expire the export by its OWN rule (a bucket's lifecycle), ISO-8601 UTC, as the backend states it. Null where the backend expires nothing by time, or cannot say: read retention.
  • retention (string, required) — How this deployment's source backend holds the export: in a bucket or a directory every API process reads, or in the memory of the one process that took it (then a connect reaches it only through that process, before it restarts). Sooner than that, a pull that writes its generation deletes it; deleting the workspace removes it.

usage (REST only)

GET /v1/public/usage — What this key holds and may do: documents by status, failures, facts, records, last activity, its role, scope and rate limit (REST only)

Who may call it. Asks for any key — roles: admin, analyst, service, viewer. A key scoped to workspaces reaches only those. A key confined to documents may call it — counts the key's own documents and what they hold; names no other.

Purpose

THE CALLER'S OWN ACCOUNT, read: who the key is (role, scope, rate limit) and, per workspace it reaches, what it holds — documents by status, failures with their reasons, facts and records held, last activity. Counted over the documents the caller may see; never a document's text, never another organization's workspace.

No MCP tool

This verb has no MCP tool because it describes the caller's own account (documents by status, failures, what they hold, its role, scope and rate limit) for the integrator's page or the SDK (usage), and an agent's menu pays for every tool on every turn.

Request

  • workspace_id (string, nullable, query) — One workspace. Left out, every workspace this key reaches, a page at a time — never a default made for the call: this verb reads and creates nothing.
  • limit (integer, query, default 20) — How many workspaces one reply describes.
  • after (string, query, default '') — next_after of the page before.
  • failures_limit (integer, query, default 20) — How many failed documents each workspace lists (newest first); failures.total counts them all.

Response

  • caller (dict, required) — WHO IS ASKING: {key_id (never the key), organization_id, role, permissions, workspace_scope (null: every workspace of the organization), document_scope (null, or {documents: how many documents the key is confined to}), principals (the end user a platform asserted, or null), rate_limit: {enabled, per: key, verbs, region, diff — each {limit, window_s}}}. verbs is the one budget every /v1/public verb draws on but region and diff (each its own) and the upload (its link was charged when minted).
  • workspaces (list of dict, required) — One entry per workspace, in id order: {workspace_id, name, default (the organization's default workspace), documents: {total, by_status: {status: count}}, failures: {total, shown: [{source_id, filename, received_at, reason}] (newest first), truncated}, facts (current facts and prose statements its documents hold), records (current relation rows they state), last_activity_at (when a document the caller may see last arrived or was read; null: none)}. Counted over the documents the caller may see. A caller confined to documents is shown a workspace it did not name only when it may see something in it.
  • next_after (string, nullable) — Pass as after for the next page of workspaces; null on the last.
  • plan (dict, nullable) — THE ORGANIZATION'S PLAN, hosted: null for an organization on no plan (every one inside your network). {name (the plan the organization is on), configured (the deployment states its limits), trial_days, limits_in_force — whose limits are in force: 'plan' (the plan's own), 'free_until_first_payment' (a paid plan not yet paid for holds the Free plan's limits until its first payment above $0) or 'trial' (until the first payment, on a plan whose trial has limits of its own), until_first_payment (null unless 'free_until_first_payment': {plan_limits — the plan's own, lifts — how they start}), trial (null unless the trial's limits are in force: {limits — the trial's, plan_limits — the plan's own for the same limits, ends_at, lifts — when and how they lift}), subscription_status (Stripe's word), standing: {state (active, grace, read_only), read_only, reasons (payment_failed, subscription_ended), read_only_from, grace_days}, subscription_ended_at, data_deleted_after (from when the organization's content is deleted, 30 days after its subscription ended; null while it runs), data_deleted_at, period (the calendar month, UTC), period_ends_at, counted_over: 'the organization', manage (how to change the plan), limits: the limits IN FORCE — {documents_held, pages_per_month, api_calls_per_month, workspaces, max_file_mb — each {limit, used, remaining}; used and remaining are null for max_file_mb, a limit per file; pages_per_month also has by_kind {read: pages read by the model, loaded: pages loaded without one} and counted_as (how a page is counted)}}. limits is null for a caller confined to documents or a call made for an end user: the counts are the whole organization's.

Schemas

Models referenced by the fields above, each once. The text beside a field is its description on the model; a model with none for its fields shows —, and the fix is at the model.

ChangeEntry

  • coordinate (dict of string, required) — —
  • kind (one ofadded,removed,changed``, required) — —
  • before (number, nullable) — —
  • after (number, nullable) — —
  • before_claim_id (string, default '') — —
  • after_claim_id (string, default '') — —
  • relative_move (number, nullable) — On changed only: |after - before| / max(|.|). The SIZE of the move, because a rounding correction and a restatement are the same boolean and different facts.
  • before_row_id (string, default '') — On a RELATION row's change, the row id in the older generation (coordinate is then {entity_id, relation, db_table, row_key} for a connected row — plus element (experiences.1) when the export row is a document stating several rows of the relation — and {entity_id, relation, row_id} otherwise).
  • after_row_id (string, default '') — On a RELATION row's change, the row id in the newer generation.
  • changed_slots (dict of dict) — On a relation changed: {slot: {before, after}} for exactly the slots (and period_from / period_to) that differ. A connected row pairs across generations on its export key, so a re-pull that changed one title is ONE changed naming that slot; a document-stated row has no key and reads as removed + added.

CheckOutcome

One check's verdict, and everything an agent needs to disagree with it.

  • kind (string, required) — —
  • verdict (one ofverified,refused,unverifiable`, required) — THREE-VALUED, and the third is the point.unverifiablemeans NOT CHECKED — an operand did not resolve, a unit was unknown, the page layer is absent. It is never a synonym forrefusedand never forverified`.
  • reason (string, default '') — A stable token, never prose: superseded, ambiguous_coordinate, value_mismatch, ...
  • computed (number, nullable) — —
  • claimed (number, nullable) — —
  • units_checked (boolean, default False) — True only where EVERY resolved operand carried a recorded unit. False means the arithmetic ran but was not dimensionally checked — reported rather than folded into the verdict, because a derivation whose units were never checked can still be arithmetically correct.
  • operands (list of [CheckOutcomeOperand](#checkoutcomeoperand)) — —
  • detail (dict) — —

CheckOutcomeOperand

  • name (string, required) — —
  • resolved (boolean, required) — —
  • value (number, nullable) — —
  • unit (string, default '') — —
  • honesty (string, default '') — —
  • handle (string, default '') — —
  • reason (string, default '') — —

CheckSpec

ONE thing the agent did that it wants recomputed.

THREE KINDS, and the fields each uses are disjoint:

  • derivation — expression over operands, compared to claimed
  • quote — is quote really in source_id (optionally at page and span)
  • existence — does anything sit at coordinate, and does that match expect

This is the half of the verb that carries no model. claim/supports above run the substrate's rule engine over a stated value; these run ARITHMETIC over addressed bytes, which is why they can return a third verdict instead of a score.

  • kind (one ofderivation,quote,existence``, required) — —
  • expression (string, default '') — e.g. income / revenue.
  • operands (list of [OperandSpec](#operandspec)) — —
  • claimed (number, nullable) — The value the agent computed. Absent = nothing to compare.
  • tolerance (number, nullable) — Relative tolerance. Defaults to 0.005 and is CLAMPED at 0.05, so a caller cannot obtain a pass by widening the target until it fits.
  • source_id (string, default '') — The document the quote is claimed from.
  • quote (string, default '') — The exact text, checked byte-for-byte.
  • page (integer, nullable) — 0-based page index.
  • span (list of integer, nullable) — [start, end] character offsets within the page.
  • coordinate (dict of string) — —
  • expect (one ofpresent,absent`, default'present'`) — —

Citation

A structured, agent-verifiable provenance handle for one fact the answer used. Richer than the flat Warrant.citations list of opaque handles: it names the exact source, the source's own label, the literal value token, the verbatim span it was read from, and — where the token is unambiguously locatable — the byte region (page, char_start, char_end) an agent can re-read with the region verb (GET /v1/public/region, MCP limma_region) to confirm the answer without trusting us.

byte_verified is the honest floor: True ONLY when the value token occurs exactly once in the source's persisted page texts and the resolved (page, char_start, char_end) slice reproduces it. False (with the offsets left None) when the token is absent or ambiguous — a fabricated offset on a trust surface is worse than none. On the CERTIFIED tiers these citations carry the structured provenance of the cells the answer was computed from.

verbatim_span applies a RELATED rule to the text: it is either text that occurs exactly once in the document or None, and the engine's own rendering of the fact is in display_text, where it makes no provenance claim. But a located quote and a resolvable LOCATOR are two claims, not one — a document ingested before page texts were persisted can support the first and not the second. byte_verified governs the locator ONLY, and when it is False the three offset fields are None while verbatim_span may still carry the document's own words under attestation_method: "located_in_extracted_text".

  • handle (string, required) — The substrate id this citation resolves to: cell:<assertion_id> / claim:<id> / semantic:<id> / passage:<n> / reextract:p<page>.
  • kind (string, required) — The source layer: 'cell' | 'claim' | 'semantic' | 'passage' | 'reextract'.
  • source_id (string, nullable) — The document (source) the fact was read from.
  • surface_predicate (string, nullable) — The source's own label words for the value (never a canonical id).
  • value_token (string, nullable) — The literal value token as PRINTED in the document, e.g. '$5,326'. NOT the stored value. A table claim's value is normalised by its table's "in millions" line (74452000000.0); the page prints 74,452. When byte_verified is True this is the exact substring at (page, char_start, char_end); when it is False this is still the token the extractor recorded as printed, and attestation_method says why it could not be located.
  • verbatim_span (string, nullable) — TEXT THAT IS ACTUALLY IN THE DOCUMENT, or None. ALWAYS text that occurs in the source document, exactly once. What it does NOT always carry is a LOCATOR, and byte_verified is how the two are told apart: * byte_verified: true — verbatim_page / verbatim_char_start / verbatim_char_end are all non-None, and re-reading verbatim_page from verbatim_char_start to verbatim_char_end with limma_region reproduces this string byte for byte. This is the only state in which the offsets mean anything, and the four fields move together within it. * byte_verified: false with attestation_method: "located_in_extracted_text" — the quote was located exactly once in the source's text but has no confirmed page: the document was ingested before page texts were persisted, or the span straddles a page boundary. The three locator fields are then None. Do not attempt a region re-read; there is nothing to re-slice. The text is still true, and you can check it with a string search. The engine's own rendering of the fact is display_text, which claims nothing. A span is also withheld when the text occurs MORE THAN ONCE in the source: it is true text, but naming one occurrence would be a guess about which one this fact came from.
  • verbatim_page (integer, nullable) — 0-based page index of verbatim_span in the source's persisted page texts.
  • verbatim_char_start (integer, nullable) — Start of verbatim_span on verbatim_page, in characters. Non-None only when byte_verified is True — see verbatim_span for the two states.
  • verbatim_char_end (integer, nullable) — End (exclusive) of verbatim_span on verbatim_page, in characters. Non-None only when byte_verified is True.
  • display_text (string, nullable) — The engine's RENDERING of this fact — assembled label + value + coordinates, or the retrieval blob the reasoner read. Human/agent readable and useful for display or matching; it makes NO claim to be document text. Read verbatim_span when you need something that is.
  • page (integer, nullable) — 0-based page index into the source's persisted page texts, when resolvable. This locates value_token (see byte_verified) — verbatim_page locates verbatim_span. They are separate resolutions and may disagree.
  • char_start (integer, nullable) — Start of value_token on page, in characters. Non-None when the token was located byte-true: byte_verified True, or attestation_method scale_unconfirmed (located, its value not confirmed); re-slice it with limma_region.
  • char_end (integer, nullable) — End (exclusive) of value_token on page, in characters. Non-None exactly when char_start is.
  • byte_verified (boolean, default False) — True iff (page, char_start, char_end) was re-sliced and reproduced value_token, and value is that token at its declared scale. The three offsets are non-None when this is True, and when the token was located but its value is not confirmed at its scale (attestation_method: scale_unconfirmed: the location is true and ships so you can read the figure's row and caption yourself; the value is not to be taken as printed) — and never otherwise, EXCEPT for a fact whose evidence is a database row (attestation_method: structured_row): a row has no span, so the offsets are None and the locator is db_table / row_key / db_column, and True means that cell was re-read at delivery and equals value_token exactly. On a page it also means the SCALE: value is value_token times the multiplier its scale was declared at (scale_declaration; a figure read as printed, 1; a model's reading, what the token's page declares for it — the scale word beside it, the caption over its table — so a figure printed in a table "($ in millions)" and stored as printed is never verified), within rounding. A per-share figure printed as $0.60 and stored as 600,000, or $229.0 million stored as 229,000,000,000, has a byte-true token and a value that is not it: byte_verified is False and attestation_method is scale_unconfirmed. It says nothing about a print that is itself wrong. Read attestation_method for the rule that resolved the location — they carry different strengths and an agent auditing us should be able to see which one it got.
  • attestation_method (string, nullable) — HOW the location was resolved, or why it was not. Never a score. Resolved, and CORROBORATED — each ties the location to this fact, not merely to the digits: span (the token sits inside the verbatim_span region, already proven byte-true and unique — the strongest) · row (the fact's own label was found on the hinted page and the token occurs exactly once in the characters that follow it) · page (the token occurs exactly once on the hinted page and the label's anchor words are on that page) · label (no page hint: the token occurs exactly once across every window opened by the label in the whole document). Resolved, but UNCORROBORATED, and only on structured_citations — a delivered fact can never carry these: document — the token occurs exactly once in the whole source and nothing else was available to check it against (no extractor label, and not a derived claim). It is the WEAKEST value here and it is named separately so you can discount it: uniqueness alone can place a citation on the wrong page, so it is used only where corroboration is impossible. clause — a prose citation, where the region locates the CLAUSE BODY rather than a value token (there is no token to locate); it is byte-true for the text and makes no claim about a number. Unresolved, and each means something different: absent — no printed form of the token occurs in the document at all, which is the transcription-error signal and the single most valuable label here · ambiguous — it occurs, but no rule could tie it to this fact's row · scale_unconfirmed — the token IS located, byte-true, but value is not that token at its declared scale (a figure read at the wrong one of thousands / millions / billions): value_token and its true offsets ship, so limma_region can show you the row and its caption, and the value is not to be taken as printed · short_token — fewer than two digits, deliberately never looked for · derived — a computed value, refused by family because its text is in no document · no_token / no_pages — nothing to attest, or nothing to attest against. None when no location check ran at all. A VALUE MENTION (kind == "value") is proven by re-reading its OWN stored position, never by a search: mention — (page, char_start, char_end) re-sliced and reproduced value_token (page None: the document frame, redeemed with limma_region without page) · mention_stale — the stored position no longer reads value_token (the document was re-read; re-enumerate), offsets None. A ROW, NOT A PAGE. A fact pulled from a table (limma_connect) is redeemed by re-reading its own row and column, never by locating a token (so no two-digit floor: a 7 verifies): structured_row — the cell re-read equals value_token · row_changed — it was read and now holds another value or NULL (the source moved since the pull: a stale-import finding) · row_unreadable — the row could not be read in this process (no adapter registered, the row gone, the database unreachable): could not check, never "checked and differs". All three carry db_table / row_key / db_column.
  • db_table (string, nullable) — The TABLE a structured-row fact was pulled from (with row_key and db_column, the fact's locator when attestation_method is structured_row / row_changed / row_unreadable). None on every page-located fact.
  • row_key (string, nullable) — The row's primary key as a JSON object string ({"metric_id": ...}), the key a re-read selects on. None on every page-located fact.
  • db_column (string, nullable) — The column holding the value. None on every page-located fact.
  • in_answer (boolean, default True) — True iff this citation is in the final answer's evidence closure (vs an incidental / superseded step).
  • tier (string, nullable) — The warrant tier this citation supports (mirrors verification_level).

ClaimSpec

The shape callers use to express a claim they want verified.

  • attribute (string, required) — Canonical attribute name (e.g. 'revenue').
  • value (number, required) — The numeric value being asserted.
  • entity (string, default '') — Entity the claim is about. Empty = wildcard.
  • period (string, default '') — Reporting period. Empty = wildcard.
  • unit (string, default '') — Optional unit string (currency code, %, etc.).

ColumnMapping

Which column supplies which axis. STATED, never guessed.

A heuristic deciding that rev_amt means revenue would be a guess wearing the provenance grade of a declaration. Being asked once is cheap; a wrong guess is a fact filed at an address nobody can find and nobody knows is wrong.

  • entity_column (string, default '') — Column supplying the entity axis.
  • period_column (string, default '') — Column supplying the period axis.
  • attribute_column (string, default '') — LONG format only: the column whose VALUE names the attribute (metric \| value). Leave empty for a WIDE table, where each value column IS an attribute. Neither shape is safely detectable, so the mapping says which one this is.
  • value_columns (list of string, required) — Columns holding facts. One row is usually several facts.
  • key_columns (list of string) — Primary key. WITHOUT IT the imported facts carry no receipt and cannot be re-read — allowed, and reported in read_errors, never silently downgraded.
  • units (dict of string) — attribute -> unit, so a derived value stays dimensionally checkable.

CompactFact

A DeliveredFact with only the fields an agent ACTS ON.

THIS IS A COST CONTROL, NOT A CAPABILITY. Every field here has the identical contract it has on DeliveredFact (read the descriptions there), and handle is the join key back to the full form: an agent that needs a prose join or an aggregate role for one fact asks for it in full. What is kept: whether the fact was found at an address or ranked (provenance), the address (entity / attribute / period), the value and its printed token, the byte receipt (page / char_start / char_end / byte_verified), the disagreement bits (contested / contested_checked / distinct_magnitudes / competing_readings), the rank — and, for a PROSE fact, its own receipt and text (verbatim_page / verbatim_char_start / verbatim_char_end, display_text, canonical_clause_type, states_values), because a clause has no page / char_* and would otherwise arrive as a handle and nothing else — and the string-slot receipts (slots).

  • handle (string, required) — —
  • kind (string, required) — —
  • provenance (string, default 'explored') — —
  • source_id (string, nullable) — —
  • entity (string, nullable) — —
  • attribute (string, nullable) — —
  • period (string, nullable) — —
  • value (string, nullable) — —
  • value_token (string, nullable) — —
  • page (integer, nullable) — —
  • char_start (integer, nullable) — —
  • char_end (integer, nullable) — —
  • byte_verified (boolean, default False) — —
  • contested (boolean, default False) — —
  • contested_checked (boolean, default False) — —
  • contested_scope (string, default 'none') — —
  • distinct_magnitudes (integer, default 0) — —
  • competing_readings (list of dict) — —
  • relevance_score (number, nullable) — —
  • verbatim_page (integer, nullable) — —
  • verbatim_char_start (integer, nullable) — —
  • verbatim_char_end (integer, nullable) — —
  • display_text (string, nullable) — —
  • canonical_clause_type (string, nullable) — —
  • states_values (list of dict) — —
  • slots (dict of [SlotReceiptOut](#slotreceiptout)) — —
  • value_kind (string, nullable) — —

ContradictionEntry

  • rule_id (string, required) — —
  • severity (string, default 'error') — —
  • attribute (string, default '') — —
  • entity (string, default '') — —
  • period (string, default '') — —
  • expected_value (number, nullable) — —
  • actual_value (number, nullable) — —
  • description (string, default '') — —

CorrectionEntry

  • row_id (string, required) — —
  • entity_id (string, default '') — —
  • relation (string, default '') — —
  • slots (dict) — —
  • period_from (string, nullable) — —
  • period_to (string, nullable) — —
  • retired_by (string, default '') — —
  • retired_at (string, default '') — —
  • retired_reason (string, default '') — —

DeliveredFact

One RELEVANT FACT handed to the agent — not just the one the answer used.

A DeliveredFact is a Citation — the byte-addressed provenance model: handle, source, verbatim span, and a re-sliceable (page, char_start, char_end) an agent can confirm without trusting us — that also carries its COORDINATES, so the agent can filter and join on them instead of re-parsing prose.

in_answer (inherited) is the discriminator: True for the facts the answer's evidence closure actually used, False for the rest of the relevant set. A fact being False is not a demotion — it is relevant evidence the answer did not use.

  • handle (string, required) — The substrate id this citation resolves to: cell:<assertion_id> / claim:<id> / semantic:<id> / passage:<n> / reextract:p<page>.
  • kind (string, required) — The source layer: 'cell' | 'claim' | 'semantic' | 'passage' | 'reextract'.
  • source_id (string, nullable) — The document (source) the fact was read from.
  • surface_predicate (string, nullable) — The source's own label words for the value (never a canonical id).
  • value_token (string, nullable) — The literal value token as PRINTED in the document, e.g. '$5,326'. NOT the stored value. A table claim's value is normalised by its table's "in millions" line (74452000000.0); the page prints 74,452. When byte_verified is True this is the exact substring at (page, char_start, char_end); when it is False this is still the token the extractor recorded as printed, and attestation_method says why it could not be located.
  • verbatim_span (string, nullable) — TEXT THAT IS ACTUALLY IN THE DOCUMENT, or None. ALWAYS text that occurs in the source document, exactly once. What it does NOT always carry is a LOCATOR, and byte_verified is how the two are told apart: * byte_verified: true — verbatim_page / verbatim_char_start / verbatim_char_end are all non-None, and re-reading verbatim_page from verbatim_char_start to verbatim_char_end with limma_region reproduces this string byte for byte. This is the only state in which the offsets mean anything, and the four fields move together within it. * byte_verified: false with attestation_method: "located_in_extracted_text" — the quote was located exactly once in the source's text but has no confirmed page: the document was ingested before page texts were persisted, or the span straddles a page boundary. The three locator fields are then None. Do not attempt a region re-read; there is nothing to re-slice. The text is still true, and you can check it with a string search. The engine's own rendering of the fact is display_text, which claims nothing. A span is also withheld when the text occurs MORE THAN ONCE in the source: it is true text, but naming one occurrence would be a guess about which one this fact came from.
  • verbatim_page (integer, nullable) — 0-based page index of verbatim_span in the source's persisted page texts.
  • verbatim_char_start (integer, nullable) — Start of verbatim_span on verbatim_page, in characters. Non-None only when byte_verified is True — see verbatim_span for the two states.
  • verbatim_char_end (integer, nullable) — End (exclusive) of verbatim_span on verbatim_page, in characters. Non-None only when byte_verified is True.
  • display_text (string, nullable) — The engine's RENDERING of this fact — assembled label + value + coordinates, or the retrieval blob the reasoner read. Human/agent readable and useful for display or matching; it makes NO claim to be document text. Read verbatim_span when you need something that is.
  • page (integer, nullable) — 0-based page index into the source's persisted page texts, when resolvable. This locates value_token (see byte_verified) — verbatim_page locates verbatim_span. They are separate resolutions and may disagree.
  • char_start (integer, nullable) — Start of value_token on page, in characters. Non-None when the token was located byte-true: byte_verified True, or attestation_method scale_unconfirmed (located, its value not confirmed); re-slice it with limma_region.
  • char_end (integer, nullable) — End (exclusive) of value_token on page, in characters. Non-None exactly when char_start is.
  • byte_verified (boolean, default False) — True iff (page, char_start, char_end) was re-sliced and reproduced value_token, and value is that token at its declared scale. The three offsets are non-None when this is True, and when the token was located but its value is not confirmed at its scale (attestation_method: scale_unconfirmed: the location is true and ships so you can read the figure's row and caption yourself; the value is not to be taken as printed) — and never otherwise, EXCEPT for a fact whose evidence is a database row (attestation_method: structured_row): a row has no span, so the offsets are None and the locator is db_table / row_key / db_column, and True means that cell was re-read at delivery and equals value_token exactly. On a page it also means the SCALE: value is value_token times the multiplier its scale was declared at (scale_declaration; a figure read as printed, 1; a model's reading, what the token's page declares for it — the scale word beside it, the caption over its table — so a figure printed in a table "($ in millions)" and stored as printed is never verified), within rounding. A per-share figure printed as $0.60 and stored as 600,000, or $229.0 million stored as 229,000,000,000, has a byte-true token and a value that is not it: byte_verified is False and attestation_method is scale_unconfirmed. It says nothing about a print that is itself wrong. Read attestation_method for the rule that resolved the location — they carry different strengths and an agent auditing us should be able to see which one it got.
  • attestation_method (string, nullable) — HOW the location was resolved, or why it was not. Never a score. Resolved, and CORROBORATED — each ties the location to this fact, not merely to the digits: span (the token sits inside the verbatim_span region, already proven byte-true and unique — the strongest) · row (the fact's own label was found on the hinted page and the token occurs exactly once in the characters that follow it) · page (the token occurs exactly once on the hinted page and the label's anchor words are on that page) · label (no page hint: the token occurs exactly once across every window opened by the label in the whole document). Resolved, but UNCORROBORATED, and only on structured_citations — a delivered fact can never carry these: document — the token occurs exactly once in the whole source and nothing else was available to check it against (no extractor label, and not a derived claim). It is the WEAKEST value here and it is named separately so you can discount it: uniqueness alone can place a citation on the wrong page, so it is used only where corroboration is impossible. clause — a prose citation, where the region locates the CLAUSE BODY rather than a value token (there is no token to locate); it is byte-true for the text and makes no claim about a number. Unresolved, and each means something different: absent — no printed form of the token occurs in the document at all, which is the transcription-error signal and the single most valuable label here · ambiguous — it occurs, but no rule could tie it to this fact's row · scale_unconfirmed — the token IS located, byte-true, but value is not that token at its declared scale (a figure read at the wrong one of thousands / millions / billions): value_token and its true offsets ship, so limma_region can show you the row and its caption, and the value is not to be taken as printed · short_token — fewer than two digits, deliberately never looked for · derived — a computed value, refused by family because its text is in no document · no_token / no_pages — nothing to attest, or nothing to attest against. None when no location check ran at all. A VALUE MENTION (kind == "value") is proven by re-reading its OWN stored position, never by a search: mention — (page, char_start, char_end) re-sliced and reproduced value_token (page None: the document frame, redeemed with limma_region without page) · mention_stale — the stored position no longer reads value_token (the document was re-read; re-enumerate), offsets None. A ROW, NOT A PAGE. A fact pulled from a table (limma_connect) is redeemed by re-reading its own row and column, never by locating a token (so no two-digit floor: a 7 verifies): structured_row — the cell re-read equals value_token · row_changed — it was read and now holds another value or NULL (the source moved since the pull: a stale-import finding) · row_unreadable — the row could not be read in this process (no adapter registered, the row gone, the database unreachable): could not check, never "checked and differs". All three carry db_table / row_key / db_column.
  • db_table (string, nullable) — The TABLE a structured-row fact was pulled from (with row_key and db_column, the fact's locator when attestation_method is structured_row / row_changed / row_unreadable). None on every page-located fact.
  • row_key (string, nullable) — The row's primary key as a JSON object string ({"metric_id": ...}), the key a re-read selects on. None on every page-located fact.
  • db_column (string, nullable) — The column holding the value. None on every page-located fact.
  • in_answer (boolean, default True) — True iff this citation is in the final answer's evidence closure (vs an incidental / superseded step).
  • tier (string, nullable) — The warrant tier this citation supports (mirrors verification_level).
  • value (string, nullable) — The fact's value as stored, distinct from the printed value_token.
  • entity (string, nullable) — The entity axis of the fact's coordinate, as the store spells it. None where the layer genuinely has none — see period.
  • attribute (string, nullable) — The attribute axis — the numeric layer's predicate (revenue, net_sales), as the store spells it; a prose fact's class is canonical_clause_type instead. None where the layer genuinely has none.
  • period (string, nullable) — The fact's coordinates. None where the layer genuinely has none — a passage window is a span of text, not an addressed fact, and inventing a coordinate for it would be worse than admitting the absence.
  • provenance (string, default 'explored') — WAS THIS FACT FOUND AT A NAMED COORDINATE, OR RANKED? addressed the fact was returned by a coordinate LOOKUP — a demand bound to a clause class and the store enumerated its members. A completeness claim is available for it, because the set it came from is ENUMERABLE and its size is knowable (matched_total). explored the fact was returned by a RANKED lane — word overlap, embeddings, the passage windows. It may be correct and may be the best thing in the store; what it can never carry is a completeness claim, because no ranking score licenses "and there are no others". DEFAULT IS explored, deliberately and in the safe direction: a lane that does not say where its fact came from has not earned a completeness claim.
  • canonical_clause_type (string, nullable) — WHAT KIND OF CLAUSE this is, for a kind='semantic' fact: the class the prose extractor assigned (debt_covenant, risk_factor, termination), or its sentinel unclassified where it segmented the clause but could not type it. It is deliberately NOT folded into attribute. attribute is the numeric layer's predicate (revenue, net_sales); a clause class is a different vocabulary from a different extractor, and merging them would leave an agent unable to tell which of the two it was reading. None means: not a prose fact · the workspace's store does not record clause classes (a legacy or trimmed workspace) · the stored class is empty. It never means "this clause has no kind" — that finding is spelled unclassified.
  • relevance_score (number, nullable) — The retrieval score that ranked this fact. Surfacing it lets an agent see WHY a fact was offered and where the delivery cut fell, rather than trusting an opaque order.
  • goal_fact_id (string, nullable) — WHICH GOAL BOUND THIS CLAUSE, or None when it was ranked. Non-null on at most ONE delivered fact per answer: the clause the root goal discharged onto, fetched by a conjunctive match on every term of its demand and byte-re-sliced out of the document's extracted text before the fact was formed — not by word overlap. It is exempt from the per-layer caps and reserved at the head of the delivery order, behind any coordinate-lookup rows. WHEN THE COORDINATE LOOKUP GOT THERE FIRST, THIS ID IS STAMPED ONTO THE LOOKUP'S ROW rather than onto a second one — the fact then carries a lookup coordinate AND this id, and it keeps its position in the lookup band. The clause is never delivered a second time, which would read to an agent as corroboration. It is a JOIN KEY. The same string is the fact_id of the kind:"goal" node in warrant.derivation.nodes[] whose status is DISCHARGED, so an agent holding this fact can read, without a second call: which discharges were tried and in what order (alternatives_tried), why each failure failed (detail.<discharge>.why), the clause and byte-range citation (citations), and the span itself (verbatim_span). When warrant.derivation.answer_fact_id equals it, this clause is also where the delivered answer came from. relevance_score on such a fact is honest and LOW — it is the ordinary lexical overlap, computed for comparability and used for nothing. A short dense clause scores below a verbose paragraph; that is precisely why this lane exists and why the score did not fetch it. Read this field, not the score, to know why the fact is here. None means "ranked, or the tree did not bind" — it never means the fact is unrelated to the goal. Only the ONE clause the root discharged onto carries it.
  • claim_level (string, nullable) — aggregate (a total) vs line_item (a component) — one of the four WHICH-READING fields documented under status.
  • table_type (string, nullable) — The statement or region the value was read from — see status.
  • group_key (string, nullable) — Ties a fact to its siblings from the same block, so an agent can reconstruct a set rather than treat rows as unrelated — see status.
  • status (string, nullable) — WHICH revenue / FY2022 IS THIS? An agent asking about revenue can receive ~40 facts sharing an attribute and a period, holding different values, with nothing to separate the income-statement line from a segment footnote from a sentence in the management discussion. The coordinates above say WHAT and WHEN; these say WHICH READING. * claim_level aggregate (a total) vs line_item (a component) * table_type the statement or region the value was read from * group_key ties a fact to its siblings from the same block, so an agent can reconstruct a set rather than treat rows as unrelated * status observed (passed the confirmation gate) vs conjectured (extracted but not promoted) — held back rather than dropped, so an agent can decide for itself None means the workspace's store does not record the property (a legacy workspace), NOT that the fact has no such property; the non-claim layers (passage, cell) genuinely have none.
  • contested (boolean, default False) — True iff the substrate holds COMPETING readings for this fact's coordinate — a disagreement extraction recorded when it de-duplicated the claims. READ THIS FIELD WITH contested_checked. True is always a finding. False is a finding ONLY when contested_checked is True; otherwise the check did not run and this field says nothing at all.
  • contested_checked (boolean, default False) — Did a disagreement check actually RUN for this fact? True says one did, and contested_scope says WHICH: the dedup record at its coordinate, the judged pairs of its document, or both — a contested: false is a finding about that scope and nothing wider. False means contested: false on this fact says nothing. That happens when: * the fact is on a layer with no dedup record (semantic / value / passage / cell) and its document's statement pairs were not all judged; * the workspace's store predates the dedup record (a legacy database); * the record could not be read (the read failed and degraded with a logged warning); * the stored record is not parsable JSON.
  • contested_scope (string, default 'none') — WHAT contested WAS CHECKED AGAINST. same_address — the readings recorded at this fact's coordinate. judged_pairs — every statement of its document filed under the same topic, or among the two nearest to it by meaning, that prints a different value of the same kind was put to the reader model, which judged whether the two state the same property (the model is named in each such reading's judged_by); a fact outside any statement, or in one with a pair left unjudged (the per-document bound, a declined verdict, no model), does not get it. same_address+judged_pairs — both. none — neither; contested_checked is false exactly then. Other documents are never compared.
  • competing_readings (list of dict) — The competing readings, RAW, in Warrant.disambiguation's reading shape ({value, source_id, table_index, row_index}) plus magnitude_group and claim_id, so an agent parses ONE structure for competing readings wherever they surface. table_index/row_index are present-but-None on this path: the stored conflict record names the competing CLAIM, not its cell coordinates, and inventing them would be a provenance lie — claim_id is the honest, resolvable substitute. Never edited or de-duplicated: an agent auditing us must see what the substrate actually holds. Read distinct_magnitudes for the normalised count. Bounded at 20 readings, drawn round-robin across the magnitude groups so a truncated list still exhibits every distinct magnitude; empty when the fact is not contested OR when it is contested and the stored detail was unusable, so the FLAG is contested, never len(competing_readings).
  • states_values (list of dict) — For a semantic (prose) fact, the NUMBERS this clause states. The prose layer and the numeric layer describe the same bytes of the same document. This is the join between them, recorded at ingest: an agent holding both "revenue increased to $5,326 million" and a claim row for revenue / FY2022 / 5326000000 can tell they are one fact. Each entry is {relationship, claim_id, value, entity, attribute, period, source_id, confirmation}. relationship is the load-bearing field and an agent must branch on it — the three values are ranked, strongest first: * states:containment — the value's recorded span nests inside this clause's byte-exact span AND this clause's text PRINTS a number equal to the value. The confirmation is deterministic and no model judged it. It matches powers of ten (0.5% tolerance), so it establishes that this sentence is where this MAGNITUDE lives — not that these exact bytes spell the value, and not the scale. * states:containment_unverified — the spans nest, and the confirmation did not hold or could not be run. Useful as a retrieval hint; NOT provenance. Read the clause yourself before relying on it. The clause's own offsets are byte-exact, but a value's recorded offsets come from the extractor and are not re-sliceable, so span nesting alone can name a sentence that prints no such number — this value keeps those edges separate rather than calling them statements. * qualifies:adjacency — the nearest clause to a value that NO clause contains. Candidacy, and never evidence that this sentence states that number. confirmation — HOW THE CLAUSE CONFIRMS THE NUMBER, AND WHETHER THE SCALE WAS PROVEN. states:containment alone does NOT distinguish "this sentence prints these digits" from "this sentence prints this magnitude divided by a million", and many confirmed edges confirm ONLY at a power of ten. That is not a rounding detail: a per-share figure can be stored a million times too large. This payload is what lets you tell them apart. {rule, factor, printed, occurrences, exact}, recomputed at delivery from the clause body and the stored value — never read back from a row that could have gone stale — and rule is one of four, strongest first: * exact_digits — some token in the clause equals the stored value exactly. * rounded — same scale, different digits: the match was bought entirely with the 0.5% tolerance. * power_of_ten — the clause prints the value DIVIDED BY factor (1e3, 1e6 or 1e9). The magnitude is located; the scale is not proven. * unconfirmed — nothing printed grounds it, or there was nothing to check with. factor is the power of ten that made the match (1.0 for the first two rules). printed is the token the clause actually prints, as a number, so you can compare it against value yourself — but THE MATCH IS SIGN-BLIND, so the identity is printed * factor == abs(value), never == value: a stored −499,000,000 is confirmed by a printed 499 at factor 1e6, because accounting text spells a negative "(499)". If the sign matters to you, read clause_body. occurrences is how many distinct printed tokens match at that factor — >1 means the clause cannot say which one it meant (a clause can print "3.9" three times and carry two different instruments' interest rates). exact IS A SEPARATE BIT FROM rule, AND YOU NEED BOTH. It is True only when no tolerance was spent. The two axes are orthogonal: a power_of_ten match can be exact ("$1.3 billion" for 1,300,000,000) and a factor-1 match can be inexact. The class held up by nothing but the 0.5% band — a stored 104,347,000,000 "confirmed" by a printed 104,262, a stored 2,023,700,000 "confirmed" by the bare calendar year 2022 — is exact == False, and it spans both rules. Filter on exact, not on rule, to find all of them. WHAT confirmation DOES NOT SAY, AND THIS IS THE IMPORTANT PART. It is a statement about DIGITS AND SCALE ONLY — never about whether the value sits on the right entity/attribute/period. exact_digits therefore means "stop wondering whether the scale was guessed", NOT "this fact is right". It is delivery metadata at SOURCED and it never gates a CERTIFIED verdict. It is also reported for qualifies:adjacency, where it answers only "does the neighbouring sentence happen to print this magnitude" — branch on relationship first, always. EMPTY MEANS ONE OF TWO THINGS, AND THE FACT CANNOT TELL YOU WHICH. Empty on every non-prose layer, and on any fact whose value never located a span — those are honest "no edge". But a workspace whose clause-to-number join was never derived (ingested before the join existed, or restored from a corpus it was never applied to) has no edges AT ALL, and its facts are equally empty. THE DISAMBIGUATOR IS OUTSIDE THE FACT. /v1/public/facts reports coverage.prose_edges_derived (true / false / null) and adds prose_edges_not_derived to gaps. Read it before concluding anything from an empty list.
  • stated_by (list of dict) — For a claim (numeric) fact, the CLAUSES that state this number. The same edge read from the other end: {relationship, semantic_claim_id, clause_body, source_id, confirmation}, with relationship and confirmation carrying the identical meaning and the identical warnings as in states_values — including that states:containment_unverified means this clause is where the number's span landed but the clause does not print it, and that confirmation.rule == "power_of_ten" means the scale is NOT proven, only the magnitude. A number delivered with the sentence that qualifies it is a materially different fact from the number alone — "$1.2 billion" versus "$1.2 billion, of which $0.9 billion related to a single customer". clause_body is the stored clause, which is verbatim in its source, not a rendering — and it is the exact text the confirmation was run against, so an agent can re-run the check itself and reproduce confirmation.printed from these very bytes. EMPTY CARRIES THE SAME AMBIGUITY AS states_values, for the same reason and with the same disambiguator: coverage.prose_edges_derived on /v1/public/facts.
  • stated_numbers (list of dict) — For a semantic / passage (prose) fact, THE NUMBERS ITS OWN TEXT PRINTS, typed and addressable. The prose layers carry no value, and they are also the layers that deliver flattened financial statements — text such as CONSOLIDATED BALANCE SHEETS / (in millions, except per share data) / December 31, / 2016 2017 / Cash and cash equivalents $ 19,334 $ 20,522 / ... with the label, the column year and the scale note all present and nothing saying which number the fact asserts. This list types them. Each entry is one row-slot of that text: * label the row's own label, reassembled across wrapped lines * value the signed number with the table's scale applied * value_token the token the document PRINTED ("$ 19,334") — the receipt * period the column header this slot sits under; "" when the table declared none. NEVER inferred from anything but the document. * period_binding column (one header per slot — geometric, the value sits in the column its header owns), column_ordinal (headers matched the VALUE count, so placeholder columns were dropped and the binding is by ordinal — a column shift would mis-align it silently), or "". BRANCH ON THIS before trusting a period, exactly as with states_values.relationship. * unit the table's own unit context ("$\|million"), or "" * char_start / char_end offsets into THIS FACT'S raw text — the string verbatim_span ships when it attests — never the document and never the truncated display_text. HOW FAR THIS GOES, AND NO FURTHER. Every entry is read by a deterministic, zero-LLM row walk, and every byte it claims is re-sliced out of the fact's own text before it ships. It is EVIDENCE, delivered with a coordinate — not a certification: nothing on a certifying path reads this field, and a typed number is not a substrate claim (it is not written to the corpus and does not survive the response). KNOWN NOISE, STATED. Every parseable row is typed, including a cover page's Washington, D.C 20549 and a statement row's footnote marker (Basic earnings per share (2) $ 0.60 ... types (2) as -2). Both are left in deliberately: the marker shape is indistinguishable from a real parenthesized value in the same column, so filtering it would delete real numbers. Read label and the row's own text before binding. Empty on the claim and cell layers, which carry a value of their own, and on prose whose text parses to no row (pure narrative types nothing — the correct answer, not a failure).
  • stated_numbers_truncated (integer, default 0) — How many typed numbers this fact's stated_numbers list DROPPED at the payload ceiling (64 per fact). 0 means stated_numbers is COMPLETE for this fact; anything above 0 means the list is not the complete set.
  • uses_defined_terms (list of dict) — For a semantic (clause) fact, THE CLAUSES THIS DOCUMENT USES TO DEFINE THE WORDS IN THIS ONE — so a delivered sentence such as "the Borrower shall not permit any Restricted Subsidiary to incur Indebtedness" arrives with a way to reach what the document says its defined terms mean. The definitions are usually in the same document, and reaching them is a string comparison, not a model call. Each entry is:: {"relationship": one of three, below "term": the string both clauses contain, or null "semantic_claim_id": the DEFINING clause "source_id", "page", "span_start", "span_end"} * mentions:defined_term — this document defines term in EXACTLY ONE clause, with a definitional construct ("X" means, shall mean, refers to, is defined as). * mentions:expanded_alias — exactly one clause, and the construct is a parenthetical alias ((the "Company")). An abbreviation, not a definition. * mentions:ambiguous_term — the document defines term in SEVERAL clauses and one entry ships per candidate. Choosing a winner would be a precedence judgement nothing here can prove; the agent applies its own rule. READ THE RELATIONSHIP LITERALLY — IT SAYS mentions. What is proven is that the term string occurs in BOTH clause bodies (word-bounded, case-sensitive) and that both bodies re-slice byte-for-byte out of the document. What is NOT proven is that this use is the DEFINED SENSE: many mentions:defined_term edges are the ordinary noun — a director's biography using the word "Board", bound to a stock plan's "Board" shall mean clause. The edge is still worth delivering, because an agent that can see both clauses decides in one fetch. Nothing here certifies anything: no relationship on this list is certifying-grade, because no string operation decides a question about meaning. NO clause_body — BY DESIGN, NOT BY OMISSION. The defining clause is LOCATED (id + source + page + byte span) so it can be fetched, cited or re-sliced. Inlining a definiens per edge would attach kilobytes of boilerplate to a single fact — the lost-in-the-middle failure this engine exists to solve. EMPTY MEANS ONE OF TWO THINGS AND THE FACT CANNOT TELL YOU WHICH, exactly as states_values: "this clause uses no term this document defines", or "the derivation never ran for this workspace". The disambiguator is outside the fact — /v1/public/facts reports coverage.definition_edges_derived and adds definition_edges_not_derived to gaps.
  • uses_defined_terms_truncated (integer, default 0) — How many definition entries this fact DROPPED at the payload ceiling (32 per clause). 0 means uses_defined_terms is COMPLETE for this fact. Entries are ordered strongest relationship first, so a truncation drops ambiguous candidates before it drops an unambiguous binding.
  • distinct_magnitudes (integer, default 0) — How many genuinely different numbers are in dispute, after folding SIGN and power-of-ten SCALE. WHY THE RAW LIST IS NOT ENOUGH. A coordinate can hold the readings [938.0, 3.7e9, 4.7e9, -4.717e9, 4.717e9], three of which are the same number at a different scale and sign. The raw list alone tells an agent "five sources disagree" when the honest statement is "three distinct magnitudes"; this field is what lets an agent tell a real disagreement from a scale or sign artefact instead of assuming either. distinct_magnitudes == 1 alongside several readings is NOT a false alarm to discard: it says the sources agree on the digits and disagree on the SCALE (a per-share figure printed as $0.60 and stored as 600,000, say). 0 when the fact is not contested (or when no reading parsed as a number).
  • aggregate_role (string, nullable) — What the DOCUMENT'S OWN ARITHMETIC says this row is: total (its value is the proven sum of the rows above it) or component (it is one of the rows summed into a total). Proved from the table's own layout, with no concept names involved — so it identifies a figure's role even when nothing in the corpus recognises its label. None means the row participates in no proven identity (the common case: most rows are in no proof), NOT that it is unimportant.
  • part_of (string, nullable) — The label of the total this row is summed into, as the extractor read it off the page. The IS-PART-OF relation itself: an agent holding a row it cannot name still learns it is part of 'Total operating costs'. None unless aggregate_role == 'component'. Labels are for a READER — join on part_of_handle, not on this string.
  • part_of_handle (string, nullable) — The HANDLE of the total part_of names, in the same cell:<id> form as this fact's own handle. The label alone is not a join: a document with two tables each printing 'Total revenue' is ambiguous over the wire, so relations are ADDRESSED rather than described. Set exactly when part_of is.
  • parts (list of string, nullable) — The labels of the rows that sum to this one, when it is a proven total, as the extractor read them off the page. The decomposition an agent would otherwise have to guess at: these are the total's operands in the document's own arithmetic, in the order they were summed (nearest row first). None unless aggregate_role == 'total'.
  • part_handles (list of string, nullable) — The handles of the rows parts names, POSITIONALLY ALIGNED with it (parts[i] is the label of part_handles[i]), so an agent can pull each operand out of the same fact set it was handed. Set exactly when parts is. An entry is "" when that one part has no resolvable address — the alignment is preserved rather than the list compacted, because a shifted index would silently mis-name every later part.
  • slots (dict of [SlotReceiptOut](#slotreceiptout)) — A RECEIPT PER STRING SLOT: {slot_name: SlotReceiptOut}. The slot names and surfaces are the {slot_name: surface} mapping an extractor or loader recorded for a relation row ({"title": "Vice President", "company": "Tesla"}). Each is located EXACTLY ONCE inside this fact's own attested sentence / row span (the verbatim_* triplet) and carries its own page-relative (page, char_start, char_end, byte_verified); read SlotReceiptOut for what each reason means. {} means the claim DECLARED no slots. It never means "the slots could not be receipted": a claim that declares slots and has no attested span ships every one of them with byte_verified: false, reason: "no_enclosing_receipt", and a slot declared with a typed value instead of a printed string ships as unprintable_surface — so the absence of a receipt is stated per slot rather than by an empty dict, and no declared name is ever dropped on the way. THE LAYER THIS IS TRUE OF: kind == "claim". A kind == "semantic" fact is a prose CLAUSE, not a relation row: it has no slot vocabulary, nothing writes slots onto it, and it ships {} because it declares none — not because a carrier is missing. Relation rows carry theirs on their own lane (a limma_facts relation_query), in this same shape.
  • scale_declaration ([ScaleDeclarationOut](#scaledeclarationout), nullable) — HOW THIS VALUE'S SCALE WAS DECIDED (read ScaleDeclarationOut): the declaration the printed figure was read under — declared_by — the multiplier applied, and where the declaration is printed, re-sliceable with limma_region. Set on every kind == "claim" fact whose stored record was read (not_recorded when its reader recorded no decision); None on prose, passage and cell facts, which carry no multiplier, and on a claim whose record could not be read.
  • name_source (string, nullable) — WHERE THIS FACT'S NAME (attribute) CAME FROM, on a kind == "claim" fact whose reader recorded it: printed — the document prints it as the value's label (a key: value line, a row label, a column header), so the name is the document's own words; structural — the document prints NO label for the value (a figure in prose), so the name is the value's ADDRESS, <topic>.<kind>.<n>: the heading it sits under (or its page / section), its kind of value, and its place among that kind's values there (payment.percentage.1: the first percentage under "Payment"). A structural name says WHERE the value is, never what it measures — read the fact's text for that. None when no reader recorded it (a claim written before the record, a deterministic table reader, a connected row).
  • value_kind (string, nullable) — THE KIND OF VALUE this fact is — date, money, percentage, duration, quantity, identifier, email, url or phone — on a fact of the value-mention layer (kind == "value", handle value:<id>), the address it was enumerated at (coordinate={"value_kind": ...}); None on every other layer. On such a fact value_token is the mention EXACTLY as printed, value its normalised form (an ISO date; "<decimal> <currency>" — an ISO code where the bytes name one, else the printed symbol: a bare $ names no single currency; a fraction; an ISO-8601 duration; "<decimal> <unit>" for a quantity; the identifier itself; an e-mail with its domain lower-cased; an E.164 phone number — or None when the bytes do not fix one, e.g. a slash date whose day/month order is unstated, a phone printed without its country code), and page / char_start / char_end its receipt: redeem it with limma_region — with page when it is set, WITHOUT page (the document frame) when it is None.

DemandCoverage

WHAT ONE PART OF THE ANSWER ASKED THE STORE FOR, AND WHAT IT GOT.

A demand is a COORDINATE — a predicate, a subject scope, and the qualifiers that were read — resolved from the compiled plan when the caller holds one and from the question string when it does not. When it binds, retrieval answers it with a WHERE on the clause class and enumerates EVERY match; when it does not, the ranked word-overlap lanes answer the question.

READ bound FIRST. It is often False, and that is not a bug. A demand binds only to a clause class the store ALREADY CONTAINS — there is no authored lexicon anywhere on this path — so bound=False means "this store holds no coordinate that names what you asked", which is a fact about the corpus and the extractor, not about the question.

matched_total vs delivered is the honesty pair. matched_total is every exact match in the store; delivered is how many this lane put into the retrieval pool. They are equal unless truncated is True. Both are 0 on an unbound demand — an unbound demand fetched nothing, and the facts the agent holds came from the ranked lanes.

  • demand_id (string, required) — Stable within one response (d0, d1, …), in resolution order.
  • predicate (string, required) — The SURFACE phrase the coordinate was read from — the quoted class, the plan's concept, or the content-word run — reported verbatim rather than normalised, so an agent can see what the resolver thought it was asked for.
  • predicate_terms (list of string) — The stemmed match key, sorted. This is what was compared against the store's own induced type vocabulary, by set equality.
  • clause_types (list of string) — The store's OWN clause-type strings this predicate bound to. Empty iff bound is False. More than one when two spellings of one concept exist in the store — both are enumerated, because the coordinate is the concept.
  • subject_source_ids (list of string) — The documents the demand was scoped to, as the CALLER restricted them — ONE PAGE of them, in id order (at most 20 unless the request asked for more), never the whole set. Empty with subject_sources_total 0 means the whole workspace was permitted; it never means no documents.
  • subject_sources_total (integer, default 0) — How many documents the demand was scoped to: the whole set, counted, of which subject_source_ids is one page. 0 when scope is workspace.
  • subject_sources_truncated (boolean, default False) — True when subject_source_ids does not hold every one of them.
  • subject_sources_next_after (string, nullable) — The next page's cursor — POST /v1/public/facts takes it as subject_sources_after. Null when no id follows this page.
  • qualifiers (dict of string) — The qualifiers read for this demand (a period, for example) — REPORTED, not filtered on: this states what was read without pretending it narrowed anything.
  • scope (string, default 'workspace') — source when the caller restricted the document set, workspace otherwise.
  • origin (string, default 'question') — plan when the coordinate came from a compiled RESOLVE step, question when it was read off the question string.
  • bound (boolean, default False) — True iff the predicate matched a clause class that exists in this store.
  • matched_total (integer, default 0) — EVERY exact match in the store — not the delivered count. This is the number an agent needs in order to tell "the store holds three of these" from "the store holds three hundred and you are seeing a page".
  • delivered (integer, default 0) — How many of them this lane put into the retrieval pool.
  • truncated (boolean, default False) — matched_total > delivered: the store holds matches this demand did not deliver.
  • rows_text_clipped (integer, default 0) — How many DELIVERED rows arrived with their clause body shortened to the lane's per-row character budget. A different loss from truncated and it must be read separately: truncated says the store holds rows you did not get, rows_text_clipped says you got the row and it is longer than the wire.
  • chars_delivered (integer, default 0) — Characters of clause body this demand put on the wire, after clipping.
  • lane (string, default 'fallback') — lookup when a coordinate answered this demand, fallback when the ranked word-overlap lanes did. It is the single field that says which retrieval mechanism produced the fact list — otherwise two responses with very different provenance are byte-indistinguishable.
  • budget_rows (integer, default 0) — What THIS demand was allowed to fetch, in rows. Stated beside what it used, so delivered can be read against a limit rather than against nothing.
  • budget_chars (integer, default 0) — What THIS demand was allowed to put on the wire, in characters.
  • budget_chars_per_row (integer, default 0) — What one row of this demand was allowed, in characters.
  • rows_dropped_by_budget (integer, default 0) — Rows this demand's OWN row budget stopped it from fetching — matched_total - fetched. This is the field that says the node was starved by its own limit, as opposed to by a sibling (rows_taken_by_other_demands) or by the store (matched_total == delivered with both small). Zero means this demand spent inside its budget.
  • rows_taken_by_other_demands (integer, default 0) — Rows this demand matched and did NOT deliver because another demand of the same question had already emitted them. It is a dedup collision, not a budget loss; truncated does not tell the two apart.
  • chars_dropped_by_budget (integer, default 0) — Characters of clause body removed by this demand's character budgets. Pairs with rows_text_clipped: that counts the rows, this counts the loss.

DocumentMappingIn

How an export's documents become rows and documents. STATED, never guessed (mode: propose reads a sample and proposes one; it is never applied for you).

  • identity ([IdentityMappingIn](#identitymappingin), nullable) — HOW RECORDS ARE RECOGNISED AS ONE THING — for the export's entity type, whatever it is (people, companies, contracts, products, accounts). A real collection holds the same thing more than once, and different things that share a name. With this block the workspace merges two records of the type ONLY on evidence a different thing cannot plausibly share — a shared identifier you declare as evidence TOGETHER with a shared row of a corroborate relation; one declared one-value-per-thing and never shared, under the SAME name where one record holds nothing else to compare (one record exported twice); or two shared rows of which one is dated — and NEVER on a name. Everything weaker is a PROPOSAL (possible_duplicates on a search; entity_duplicate_proposals on state) that merges nothing until somebody confirms it (resolve). A contradiction (two values of an identifier a thing holds one of; two places the vocabulary puts apart) always wins. No row is rewritten — a merge is undone by one decision. Stating or changing the block never rewrites the collection: the next pull records it and re-resolves. Needs a name to compare: name_path, or a person's given/family paths.
  • people (dict, nullable) — RETIRED — refused with the reason. State the same block as identity with "name_kind": "person" and each identifier's compare_as.
  • key_path (string, nullable, default '_id') — Unique per document: the receipt's row key and what a re-sync knows a document by. A duplicate refuses the pull.
  • entity_path (string, nullable, default '_id') — The SUBJECT of every row — and of every document registered from the export. The same as the key for a collection of people; another field for a collection of roles or transcripts (expert_id).
  • name_path (string, default '') — The subject's display name: read for every record that names its subject, kept per subject, and what limma_search names a person by.
  • as_of_path (string, default '') — A per-document date witnessing every interval with no end (updated_at). Not both this and as_of.
  • as_of (string, default '') — A constant YYYY-MM witness for the whole pull.
  • relations (list of [DocumentRelationIn](#documentrelationin)) — —
  • documents (list of [DocumentTextIn](#documenttextin)) — —
  • max_quarantine_rate (number, default 0.05) — A value the loader cannot read is set aside and named, never guessed; past this share of faults (and at least 3) in any one population — the documents, a relation's elements, a slot's values — the WHOLE pull is refused: that many faults is a wrong mapping, not dirty data.
  • separators (dict of string, nullable) — {field: separator} — fields whose ONE value is a LIST written with a separator (a CSV cell "a; b; c"), read as an array whose items are the parts (stripped), so an entry iterates them (iterate: "tags[]", slot_paths: {<slot>: "$"}). NOTHING is split unless named here: read as one value, a list answers a search for one of its items none. A proposal over a CSV or a sheet leaves null where a column looks like a list, and a pull refuses the null. mode: propose takes a mapping that holds ONLY this key, and drafts over the split records. Part of the line's identity: a changed separator is a changed mapping (replaces).
  • max_shrink_rate (number, default 0.1) — An export holding this much fewer documents (or subjects) than the generation it would retire — and at least 3 fewer — is refused unless allow_shrink: a truncated nightly file must not delete a collection. The same rule holds for each mapped documents path, counted by record: an export in which that many fewer records carry a text there than hold a document registered from it (a job that dropped the bio field) is refused too, before anything is retired.

DocumentRelationIn

Where ONE relation's rows are in a document. STATED, never guessed.

  • relation (string, required) — A relation the workspace's schema declares.
  • iterate (string, nullable, default '') — "" — the document itself states at most one row (a person-level country); or "<array path>[]" — each element of that array is a row (experiences[]). ONE LEVEL, and no key that is data: rows held in an array INSIDE an array's elements, or under an object keyed by ids, can be read by no mapping — reshape the export so they stand in a first-level array ($unwind, $objectToArray). A null (a proposal's question: another place may hold this relation's rows) is refused: name the array, or remove the key — or, where the entry holds nothing else, state the entry or remove it.
  • slot_paths (dict of string, nullable, required) — {slot: path} — RELATIVE TO THE ELEMENT for an iterated relation, where "$" is the element itself (an array of scalars). Dotted, no index. A null (an unresolved proposal) is refused: choose a path or drop the slot.
  • period_from_path (string, nullable, default '') — The start date's path.
  • period_to_path (string, nullable, default '') — The end date's path. A null (a proposal's question: the end may be a key of the array it did not list) is refused: state the path, or remove the key if the rows carry no end date.
  • period_text_path (string, nullable, default '') — ONE timeline string instead of two dates ("Jan 2020 - ", "Mar 2015 – Dec 2019"). Not with the two paths above.
  • period_end (one ofexclusive,inclusive, nullable) — REQUIRED when an end can be read: exclusive — the end is the first day NOT held; inclusive — the last day held. Never defaulted: either default is a month wrong on every closed interval for somebody.
  • missing_end (one ofunknown,open_if_latest,open, nullable) — REQUIRED when a start is mapped — what a MISSING end means: unknown (nothing is claimed: the row is kept undated); open_if_latest (still held only when no other element of the array starts later — the safe reading of a CV; an ITERATED relation only: refused where the document itself states the row, which has no other element to be later); open (still held — so a role that ended and whose end is not written reads as held today). An explicit "present" is always open.
  • empty_means (one ofnot_recorded,none, nullable, default 'not_recorded') — What an EMPTY array says — one that holds nothing: [], or only elements that hold nothing — not_recorded — nothing; none — "there are none", on your statement. (An element that holds values and none where any entry OF THIS RELATION over its array reads is NOT empty — whoever else reads it: it is quarantined element_unread for this relation and its entity is never none for it; see unread_means.) EACH PLACE ANSWERS FOR ITSELF: where a relation is read from more than one place (two arrays; an array and the document's own fields), an entity is none only when every place gave it a row or reads "none" — an entity with rows from one array and another left EMPTY at not_recorded keeps its rows and is never none for the rest. (Two entries over ONE array are two readings of one place: it has spoken when either stated a row, and reads "none" only when both do.)
  • absent_means (one ofnot_recorded,none, nullable, default 'not_recorded') — What a document with NO such array says (no key, or a null): not_recorded — nothing (the default); none — "there are none": the statement of a store that writes no key where it would write an empty array (and then a record that LOSES the key reads as having none: the pull cannot tell the two apart). An iterated relation only. Judged place by place, as empty_means is.
  • unread_means (one ofwithheld,nothing, nullable, default 'withheld') — What an ELEMENT that holds values and none where this entry reads says (no slot, no period — at any entry OF THIS RELATION that reads its array; judged relation by relation: an element only ANOTHER relation's entry reads is unread for this one): withheld — it was NOT READ (the default): quarantined element_unread, its entity never none for it, and past max_quarantine_rate the pull is refused; nothing — it is no row of this relation, on your statement (a blank subdocument that carries only an id, a note; a relation only SOME elements state — a key a second relation reads from the roles): counted as an empty element (elements_read_as_nothing). An element that gives the entry its PERIOD and no slot value is a fault of its own WHATEVER this states (tenure_of_nothing: a period is evidence of a row) — a relation only some elements state is read without the elements' dates. Stated nothing, a ROW written under other keys reads as no row: the pull cannot tell the two apart, and says how many such elements there are on the first pull and whenever it read more of them than the generation that answers was written with. An iterated relation only; where several entries of the relation read the array, all must state it. A proposal leaves it null, with the counts, wherever sampled elements are such.
  • confirmed (boolean, nullable, default True) — A PROPOSAL IS A DRAFT. Every entry mode: propose writes for you carries "confirmed": null, and a pull is REFUSED while it does: a mapping guessed from a sample is never loaded as it came. Answer the entry's other nulls, check the paths it reads, read what it does NOT read (proposal.unresolved: relations[<relation>].confirmed, whose not_read lists the fields of the same place that could be a row's date or its party), then set true. An entry you write yourself needs no such key.

DocumentSourceIn

An EXPORT FILE: a document-store export (mongoexport), or a CSV / TSV file or one sheet of an XLSX workbook — one row, one record. Named by EXACTLY ONE of path (a file on the API host) and export_id (a file uploaded to this workspace).

  • path (string, nullable) — ABSOLUTE path of the export file on the API host: a mongoexport file — one document per line (the default) or a JSON array (--jsonArray), Extended JSON v2 — or a CSV / TSV file, or an XLSX workbook, whose rows are records (the header row names the fields). Its format is format, or its NAME: .csv, .tsv, .xlsx (.xls, .xlsb, .ods are refused), and any other name is JSON — never guessed from the content. It must lie under a directory an operator listed in EPISTEM_CONNECT_ALLOWED_EXPORT_DIRS (none by default: export files are then refused); a relative path, a path that resolves outside those directories (through .. or a symlink), a directory, a pipe, and a workspace store are refused. Not with export_id.
  • export_id (string, nullable) — INSTEAD OF path: the export_id the upload door answered (POST /v1/public/upload_export, a multipart upload of the file — over HTTP or the SDK's upload_export, never through a tool call) for THIS workspace. The file is read by the same reader as a path of the uploaded file's name (its format is that name's, or format), from a private temporary copy the server removes when the connect ends. A PULL that writes its generation deletes the export (export.deleted on the response); a proposal and a refused pull leave it for the next call. An id this workspace does not hold — never uploaded here, already pulled, expired, or another workspace's — is the same 404 export_not_found. No route returns the bytes, and nothing re-reads them after the pull: a row read from an upload re-reads row_unreadable — upload tonight's export and connect it again to re-check. Not with path.
  • format (one ofjson,jsonl,json_array,csv,tsv,xlsx, nullable) — The file's format, stated — else read from its name. json is either JSON layout; jsonl / json_array are refused when the file has the other. A CSV is UTF-8 (a byte-order mark is fine; any other encoding is refused, naming it); every cell is TEXT, and an EMPTY cell is a missing value. An XLSX cell keeps its type (numbers, booleans; a cell formatted as a date is a date; a formula is its stored result).
  • delimiter (string, nullable) — CSV / TSV only: ,, ; or a tab (\t, or comma / semicolon / tab). Unstated, it is read from the HEADER LINE ONLY — the one of the three that stands there outside quotes — or from Excel's sep= first line; a header holding none or two is refused: state it. proposal.source says which was used, and why.
  • sheet (string, nullable) — XLSX only: the sheet to read. One sheet is one source; a workbook of several sheets is refused unless this names one (the refusal lists them).
  • hidden_rows (one ofread,skip, nullable) — XLSX only: what a pull does with rows the sheet HIDES — read loads them as records, skip leaves them out. A sheet with hidden rows that hold values is refused until this is stated (a hidden row may be one the export filtered out); a proposal reads them and asks.
  • collection (string, required) — The LABEL this export is known by: letters, digits, _, ., - (1–120, starting with a letter or digit). It is the db_table of every receipt, the identity of the line of generations (tonight's file under a new name is the next generation of last night's — the path is NOT the identity), and the prefix of each registered document's filename. One label holds one line: load shards of one collection under distinct labels, or concatenate them.

DocumentTextIn

A prose field of a document, registered as its own document.

  • path (string, required) — The field (bio), or one level of iteration (transcripts[].text, notes[]).
  • subject_path (string, default '') — The field naming the document's subject; default entity_path.
  • kind (string, default '') — A document kind the workspace declares (profile, transcript); empty is its default.
  • read (one ofdefer,queue,retire`, default'defer') —defer(default) — stored with its text, readable throughregionand by a criterion search, read by NO model (on a hosted plan it counts as loaded: 30,000 characters a page, added up over the pull with its records, and only when the text is new or changed).queue— at mostmax_queueddocuments are queued for a FULL ingest (it spends model calls, and counts as read); with no model available they are deferred and say why.retire` — retire every document registered from this path.
  • max_queued (integer, nullable) — REQUIRED with read: queue: how many may be queued.
  • as_of_path (string, default '') — The document's own date (for an iterated path: relative to the element, e.g. a call's date). Default: the mapping's witness.
  • turns (dict, nullable) — A TRANSCRIPT's speaker format, stated: {label_pattern, interviewer_labels} — a regex with a named group label, matched at a line start, and the labels that are NOT the subject.

EntityDecisionIn

  • entity_ids (list of string, required) — The TWO records the decision is about (record ids — a members[] or possible_duplicates[].entity_ids entry), both of the call's entity_type.
  • decision (one ofsame_entity,different_entities`, required) —same_entity— they are one thing: merged from now on, whatever their names or identifiers say (one that moved country is thenundeterminedfor the relation the two records disagree on).different_entities` — they are two: never merged and never proposed again, whatever the evidence.
  • reason (string, required) — Why — recorded on the decision and in its audit event.
  • actor (string, required) — Who decides, as the caller states it; the event also records the key that authenticated.

GenerationEntry

  • source_id (string, required) — —
  • filename (string, default '') — —
  • file_hash (string, default '') — —
  • ingested_at (string, default '') — —
  • is_current (boolean, default True) — —
  • superseded_by (string, default '') — —
  • duplicate_of (string, default '') — Set instead of superseded_by when this upload was found BYTE-IDENTICAL to a document already held and was folded into it. Kept distinct because 'we merged your upload' and 'your upload replaced it' are different answers to an operator asking where a fact went.
  • superseded_at (string, default '') — —

GridAccountingSummary

The per-document grid account, SUMMARY form (counts only): what the document's tables held, what the deterministic grid reader emitted, where every emitted fact went (post_reader / merge), what the store now holds (stored), the printed-quantity denominator (printed) against what a current claim states (covered), the residue by bucket (residue — counts_by_bucket + per_page; the sampled items ride the full form), and row and figure counts over the document's primary statements, by outcome (census). reconciles is the reader-to-merge identity; False names a leak, and reconciliation carries both sides. error is set — and every count absent — when the accounting itself failed on that ingest, so "not computed" is distinguishable from "computed and empty".

  • version (integer, default 1) — —
  • origin (string, default '') — —
  • run_id (string, nullable) — —
  • computed_at (string, nullable) — —
  • source_id (string, default '') — —
  • error (string, nullable) — —
  • page_count (integer, default 0) — —
  • statement_pages (list of dict) — —
  • tables (dict) — —
  • rows (dict) — —
  • cells (dict) — —
  • post_reader (dict) — —
  • merge (dict) — —
  • stored (dict) — —
  • reconciles (boolean, nullable) — —
  • reconciliation (dict) — —
  • printed (dict) — —
  • covered (dict) — —
  • residue (dict) — —
  • census (dict) — —

IdentifierPathIn

One identifier field of a record: what it is, how two values are compared, and the two judgements only the data's owner can make about it. None has a default.

  • path (string, required) — The field (registration_no), or <array>[] for an array of values (emails[]). Its VALUE is never stored or returned: a key (a hash) is.
  • kind (string, required) — WHAT the identifier is, in your own word — registration_number, lei, vat_id, web_domain, contract_number, sku, gtin, medical_record_number, email, profile_url (lower-case letters, digits and _). Values of two kinds never match each other.
  • compare_as (one ofemail,url,domain,code,text, nullable) — REQUIRED. How two values are compared: email (case ignored; a shared mailbox such as info@… is never evidence), url (a profile link: scheme, www., query, trailing slash and case ignored), domain (a web domain: the host of what is written, www. and case ignored), code (an issued number — registration, VAT, LEI, contract, SKU/GTIN, record number: case and every separator ignored), text (case, accents and separators ignored).
  • shared_value_is_evidence (boolean, nullable) — REQUIRED. true: one value of this field is NEVER held by two different things — so a value two records share is evidence they are one. false: it proves nothing (an office telephone, a group switchboard). Never defaulted: a wrong true merges two things.
  • differing_values_veto (boolean, nullable) — REQUIRED. true: one thing NEVER holds two values of this field — so two different values rule a merge out (a registration number, a profile URL). false: they rule out nothing (e-mail addresses: people change jobs).

IdentityMappingIn

How this export's records are recognised as ONE THING — the identity block of the export's entity type (the subject kind of its relations). Stated, never inferred; without it nothing of the type is merged and every record is its own entity.

  • name_kind (one ofperson,organisation,label, nullable) — REQUIRED. How the type's names are compared. person: given, middle and family names, titles and credentials ('Dr. Olivia C. Ellis, MBA' may be 'Olivia Ellis'). organisation: the legal form is read apart from the name — 'Solvane' and 'Solvane K.K.' may be one company, 'ACME LIMITED' and 'Acme Ltd.' are one name, and 'Ferrum Systems AG' and 'Ferrum Systems Ltd' are TWO companies (a different legal form is never evidence of sameness). label: a product name or a contract title — the same text, or different. A name never merges anything on its own.
  • given_name_path (string, default '') — A PERSON's given-name field, with family_name_path (both or neither). With them nobody guesses which word of a display name is the family name.
  • family_name_path (string, default '') — A person's family-name field.
  • identifiers (list of [IdentifierPathIn](#identifierpathin)) — —
  • corroborate (list of string) — Relations whose SHARED ROWS are evidence: two records that both state the same row (a named party AND one other slot equal, nothing disagreeing, periods that can coincide) — the same role at the same company, the same party to the same contract.
  • exclusive (list of list of string) — [relation, slot] pairs one thing holds ONE value of (where it is based). Two values the vocabulary places apart rule a merge out; 'UK' and 'United Kingdom' are one place. The slot needs a vocabulary, or the pull is refused.
  • confirmed (boolean, nullable, default True) — A PROPOSED block is a draft: mode: propose offers one BESIDE the proposed mapping (proposal.identity.block) with "confirmed": null and every judgement of every identifier null, and a pull is REFUSED while any is. Answer them, then set true. A block you write needs no key.

NameDecisionIn

  • name (string, required) — The NAME the proposal is on — a stored spelling facts carry (a labels.names[].attributes entry; the name of a listed proposal).
  • candidate (string, required) — The name it was PROPOSED as (the proposal's candidate).
  • decision (one ofaccept,reject,undo`, required) —accept—namebecomes an ALIAS ofcandidate: its facts are delivered atcandidate(limma_factscoordinate.attribute) and published under it (labels); NO stored fact or receipt changes.candidatemust be a name the workspace's vocabulary registers.reject— the proposal is closed and never counted open again.undo` — the pair is an open proposal again, whichever was decided.
  • reason (string, required) — Why — recorded on the decision and in its audit event.
  • actor (string, required) — Who decides, as the caller states it; the event also records the key that authenticated.

OperandSpec

One input to a derivation, named the way the agent got it from us.

Either a HANDLE (cell:<claim_id> — exactly what /facts returned) or a COORDINATE. A coordinate matching more than one fact is UNVERIFIABLE, never first-wins: which of the three revenues did you mean is the agent's question.

  • name (string, required) — The name this operand has in expression.
  • handle (string, default '') — cell:<claim_id>, as returned by /facts.
  • attribute (string, default '') — Coordinate axis (used when no handle).
  • period (string, default '') — Coordinate axis.
  • entity (string, default '') — Coordinate axis.

PackDependencyApplied

  • bundle_id (string, required) — —
  • bundle_version (string, required) — —
  • pack (string, default '') — The pack name its relations apply under.
  • relations_loaded (list of string) — Relations the DEPENDENCY made addressable here, beside the applied pack's own relations_loaded.

PageNotRead

One page of a document no reader read, as recorded on the source. page_number is 1-based, or null for a DOCUMENT-LEVEL refusal (a region that names no page: an image uploaded as an image, a PDF read whole); region_ids join to the document's extraction_health.failures.

  • page_number (integer, nullable, required) — The 1-based page the refused region is — or null when that region names no page because it IS the whole document (an image uploaded as an image, a PDF read whole): the refusal then covers every page of it. 0 only on a pages_not_read_unreadable record.
  • reason (string, required) — Why it was not read: one of image_input_unsupported, model_call_failed, model_calls_disabled, model_not_configured, model_output_unreadable, model_unavailable, page_render_failed, vision_page_cap. model_unavailable: the provider stayed unavailable through its retries; a later EXTRACTION can read the page. A plain re-send of the same bytes to limma_ingest returns the finished document without re-reading it: send it again with retry: true to queue one extraction that reads it. limma_ingest carries text only: a PDF or an image is re-sent to the upload door, POST /v1/workspaces/{id}/documents, with the form field retry=true (the operator's POST /api/workspaces/{id}/re-extract queues the same for every source). vision_page_cap: the page is past LIMMA_VISION_PDF_MAX_PAGES and no vision read was queued. page_render_failed: the page could not be rendered to an image for its vision read. The others change only with the configuration or name a fault in detail. pages_not_read_unreadable: the stored record itself does not validate.
  • detail (string, default '') — The reader's own sentence: which page or image, and why it was not read.
  • region_ids (list of string) — The refused regions over this page (two readers can cover one page); each joins to the document's extraction_health.failures[].region_id.

PlanStepOut

  • id (string, required) — —
  • op (string, required) — —
  • args (dict) — —

PublicAskSegment

One segment of a synthesized answer, with its own verification level.

The response's answer string joins the segments' prose; read the segments to tell a refused part of an answer from a verified one.

Fields

prose The segment text. For verification_level= 'refused' this is the refusal line ("I cannot verify ..."); for other levels this is the model's grounded prose. citations Statement IDs cited by this segment. May be empty (segment was produced without any cited Statement — caller can treat as ungrounded). verification_level Granularity of downstream verification: 'value' (numeric / period / comparative match), 'citation' (citation resolved but no value-bearing assertion), 'refused' (value-level check failed → fail-CLOSED), or 'unverified' (no verifier ran).

  • prose (string, required) — —
  • citations (list of string) — —
  • verification_level (one ofunverified,value,citation,refused`, default'unverified'`) — —

ReconciledDocument

  • external_id (string, required) — —
  • source_id (string, required) — —
  • external_version (string, nullable) — —
  • content_hash (string, nullable) — sha256 of the bytes received.
  • recorded_at (string, nullable) — —
  • processing_status (string, nullable) — —
  • access (list of string, nullable) — The document's access list; null = no list, [] = nobody.

RefusalReason

Machine-readable reason an answer was refused — the agent's next action.

(no fields)

RelationMappingIn

Which export columns supply one relation's rows. STATED, never guessed.

The typed slots come from the workspace's ACTIVE relation schema (the pack that declares relation); this names only which column holds each of them.

  • relation (string, required) — A relation the workspace's schema declares.
  • entity_column (string, required) — Column holding the row's subject.
  • slot_columns (dict of string, required) — {slot: column} for the declared slots this export carries; an unmapped declared slot is absent from every row.
  • key_columns (list of string, required) — Primary key — REQUIRED: a relation row's receipt IS its row key, so a keyless export cannot be loaded as rows.
  • period_from_column (string, default '') — YYYY-MM (or YYYY-MM-DD, truncated) start of the half-open interval. A bare year is refused.
  • period_to_column (string, default '') — YYYY-MM end (exclusive). NULL beside a start is an open interval and needs a witness (as_of_column / as_of).
  • as_of_column (string, default '') — Column witnessing an open interval.
  • as_of (string, default '') — A constant YYYY-MM witness for the whole pull (not both this and as_of_column).
  • coverage_columns (dict of string) — {relation: column} — OTHER relations this export declares it looked at per entity: a true value writes that relation's coverage row for the row's entity, false/NULL writes none, a non-boolean rejects the pull. Every entity pulled is covered for relation itself; a row with every slot and period NULL writes that coverage and no row (an absence row). The export is READ for relation plus every key here, whatever the flags say (relations.relations_read in the response): reading a relation the pack states must carry a required one (required: true — every other relation of its subject kind in the pack; a list — the named ones), without reading the required one, refuses the pull (422 relation_rows_refused), however many relations the export carries; a mapped required column false for everyone loads, and those entities read not_recorded. Each coverage row carries the export's as_of (the latest witness among the rows that declared it).

RelationQuerySpec

The CATEGORICAL address: {relation, where, active_in, entity_id}.

A query the grammar refuses — a substring op, an ambiguous number, a bare year on a period bound — is a 422 that names the fault.

  • relation (string, required) — The relation to enumerate — a name the workspace's loaded pack schema declares (held_role, screening, ...). An undeclared name is not an error: the response says bound: false with unbound_because: relation_unknown:<name>.
  • where (list of list of any) — Closed predicates over the relation's typed slots, each a [slot, op, value] triple. Ops: eq, ne, in / not_in (value is a non-empty list), gte, lte. contains / like / regex are REJECTED (422): a substring match has no closed set to count over. A clause on a TERM slot is EXPANDED through the workspace's vocabulary before it runs — a synonym to its term (Head of → Director), a term to itself and everything below it (DACH → Germany, Austria, Switzerland), gte / lte to the ranked set (gte VP → VP, SVP, C-level) — and relations.expansion prints every resolution. A term slot whose axis has NO vocabulary — or a ne / not_in over a surface the vocabulary uses for two terms (its complement would omit rows under either reading) — is named in relations.unbound_slots: an equality clause on it runs AS WRITTEN and the completeness licence is withheld (narrowed_by: vocabulary_unbound:<slot>); a gte / lte on it cannot run (an order is a vocabulary fact) and the query is unbound. Term AND entity_ref values are whitespace-collapsed and case-folded on both sides (novalis pharma ag finds Novalis Pharma AG); number values compare numerically and must be written unambiguously (1000, 1,000.50; 1,5 is a 422). A stored date sits at the START of what it names and a date value in where names the WHOLE of what it names under EVERY op: ["month", "lte", "2021"] reaches 2021-12; eq / in on "2021" keep the rows dated anywhere in 2021 (the same set as gte + lte on "2021"), ne / not_in the rows dated outside it; a day (2024-02-14) names only itself. A gte / lte on a term slot whose value the axis's order does not know is unbound too (relations.unbound_slots) — the set above an unknown surface is undefined, never empty — while eq on it is a true empty. Beside the declared slots, period_from and period_to are addressable with the same ops on YYYY-MM values (a bare year is a 422), so one (entity, company, period) address can be named exactly; a NULL period bound is not matched by any op.
  • active_in (integer, nullable) — A four-digit year. Keeps rows whose half-open interval [period_from, period_to) overlaps it: period_from <= Y-12 and (period_to null or period_to > Y-01). A role ending 2019-01 is NOT active in 2019; one ending 2019-02 is. Rows with no period_from are never active: they are counted in relations.undated, and when that is above zero the completeness licence is WITHHELD with narrowed_by: undated_rows — a count (a zero above all) taken over rows the date predicate could not see describes less than you can see. Counted whatever else the where names — a period_from / period_to clause or a slot the row does not record cannot see it either — less any row a clause FAILS on the record, and never a second time under period_unjudged / slot_unrecorded, whose reads keep active_in.
  • entity_id (string, nullable) — Narrow to one entity and get the PER-ENTITY verdict: bound is whether any load looked for this relation on this entity. An entity nobody looked at is NOT RECORDED — count is null and not_recorded lists it — never 0 rows. relations.absence.entity carries the verdict itself: kind: none (a coverage row says the entity was read for this relation and holds zero rows of it; bound_by names the source, denominator is 0), kind: not_recorded (no coverage row; bound_by and denominator null), or null when the entity holds rows of the relation.

ScaleDeclarationOut

HOW A VALUE'S SCALE WAS DECIDED — the declaration the reader read the printed figure under, and where the document prints it.

A stored value is the printed figure times a multiplier ("74,452" under "(in millions)" is stored 74,452,000,000). The multiplier is the reader's decision, and this is the account of it an agent can check: declared_by names what declared the scale, multiplier what was applied, and (page, char_start, char_end) where the declaration is printed — re-read it with limma_region and compare with text.

  • declared_by (string, required) — What declared the scale the value was read under: table_head (the table's caption, head or a header cell) · footnote (a footnote the table's head marks) · page_lead_in (a page line above the table — its lead-in, a heading, or the previous page's lead-in that the table continues) · document_convention (a standing sentence: "Unless otherwise noted, tabular dollars are in millions") · row_label (the row's own label: "Gross Payment Volume (GPV) (in millions)" — or a unit of its own that is not a scale, "Expected life (in years)", "Gross margin (%)", "(x)", which keeps every caption's scale off the row: multiplier 1; a column header's own unit is table_head with multiplier 1) · cell (the figure's own word: "$400 million") · prose_restatement (nothing declared, and the text under the table restates its figures with a scale word) · as_printed (nothing declared: the value is the printed figure) · not_recorded (the reader recorded no decision — a connected row, a model fact read before decisions were recorded; nothing is known about the scale, which is NOT the same as as_printed). A MODEL reader's fact records what the text it read declares for the figure (its scale decision): cell, row_label or page_lead_in as above, or none_found (no declaration scopes the figure there — prose, a footnote, a table no caption on that text opens) or ambiguous (the table's head declares several, one per column, or the declaration's own exception may cover it); multiplier is then the factor its value carries, and byte_verified holds only when the figure's page admits it. A declaration may be in force and not applied: under "(in millions, except per share data)" an EPS of 3.79 is declared_by: table_head with multiplier: 1 — the declaration's own exception (it is in text), a per-share label or a % kept it as printed.
  • multiplier (integer, nullable) — The factor the stored value carries over the printed value_token: 1 means stored as printed, 1000000 means the figure was read in millions. None when declared_by is not_recorded.
  • page (integer, nullable) — 0-based page index of the declaration in the source's persisted page texts. Non-None exactly when byte_verified is True.
  • char_start (integer, nullable) — Start of the declaration on page, in characters; re-slice it with limma_region. Non-None exactly when byte_verified is True.
  • char_end (integer, nullable) — End (exclusive) of the declaration on page, in characters. Non-None exactly when byte_verified is True.
  • text (string, nullable) — The declaring text as the reader read it: "(Amounts in millions, except per share data)", "(a) In billions.", "million" after "$400". Present whenever the reader read the declaration off a page, verified or not; None for as_printed, not_recorded and a declaration with no place on the page (a page-level hint).
  • byte_verified (boolean, default False) — True iff (page, char_start, char_end) was re-sliced from the persisted page texts at delivery and reproduced text byte for byte. False with the offsets None when it could not be: no page layer, the document re-ingested since it was read, or nothing recorded to re-slice.

SearchConstraintSpec

One condition, in the compiled form limma_search returns under compiled.constraints — so an agent can edit a compile and send it back.

  • relation (string, required) — A relation the workspace's pack declares (held_role, screening, ...). An undeclared one is listed under gaps.unbound as relation_unknown:<name> and does not run.
  • slot (string, required) — A slot of that relation, or period_from / period_to. An undeclared slot is slot_unknown:<name>, unbound.
  • op (one ofeq,ne,in,not_in,gte,lte`, required) — A closed predicate. On a term slot every op goes through the workspace vocabulary (expansionprints each resolution);gte/lte` on a term slot need an ORDERED axis.
  • value (any, required) — One scalar, or a non-empty list for in / not_in. A value the slot's type cannot compare is a 422.
  • brief_span (list of integer, nullable) — [start, end) into the brief this came from, carried as given: there is no brief on this path to check it against.
  • required (boolean, default True) — false makes the condition a PREFERENCE: it never removes an entity from the set and counts toward the rank.

SearchCriteriaBudget

What ONE search may spend reading documents for its criterion.

  • max_reads (integer, default 100) — Model reads this call may START — they are read in the background, to the end, under this budget. More are needed → NOTHING is read (over_budget, with the estimate). 0 = reuse stored judgments only (open to a read-only key): it starts nothing and never waits.
  • max_usd (number, nullable, default 0.5) — ESTIMATED cost of the reads above it → nothing is read. null = accept spend this deployment cannot price. It binds the ESTIMATE, not every cent: the one call that splits a new text with no facets is made before the plan is priced; and when the estimate proves low, reading stops at the first read that crosses it, plus the reads already in flight (over_budget_during_read).
  • max_seconds (number, nullable) — How long THIS CALL waits: it returns by this, from its arrival (default and ceiling: the deployment's wall, 40 s) — sooner when the read finishes. The read goes on without the call (criteria[].background); a reply that says partial is progress, and calling again waits for the rest. (A deployment that reads only while a call is open, LIMMA_CRITERIA_BACKGROUND=0, leaves what does not fit for the next call, and there max_seconds with max_reads > 0 must exceed its read time — 17 s by default; 422 otherwise.)
  • max_read_seconds (number, nullable) — How long the READ this call starts may go on in the background (default and ceiling: the deployment's, 900 s). Past it no further document is started (partial, time_budget); the next call continues.
  • retry_failed (boolean, default False) — Read again the documents whose earlier read failed for good (a reply cut short, an unparsable one). Never one whose judgment was CORRECTED.

SearchCriterionSpec

ONE condition the workspace's relations cannot express, READ from documents.

  • text (string, nullable) — The condition, in plain words ("20+ years marketing baby formula in European markets"). Exactly one of text and address. The text is workspace metadata, visible to every key of the workspace: write the condition, never who is asking.
  • address (string, nullable) — crit_<16 hex> — reuse a stored criterion EXACTLY (every response returns it). Two phrasings of one niche are two criteria and are never merged: a text's identity ignores case and spacing, NOT punctuation — the same words with a final full stop are another criterion, read (and paid for) again. Reuse by address.
  • facets (list of [SearchFacetSpec](#searchfacetspec), nullable) — Your own decomposition of text: no model call decomposes it. Omitted, ONE model call does. A caller's facets and a model's decomposition of one text are two criteria. Your facets are a STATEMENT: never cut, added to or refused for what they leave out (that rides every match as unchecked_words). A model's decomposition is read only when the reader can prove it covers the text; when it cannot (decomposition_incomplete), the block's facets + suggested go here, as they are or edited — of suggested entries that carry a reading, ONE reading's.
  • subject_kind (string, nullable) — The kind of entity the criterion is about. Taken from the constraints when there are any; needed only for a search with no constraint in a workspace whose relations are about more than one kind.
  • record_check ([SearchRecordCheckSpec](#searchrecordcheckspec), nullable) — The relation that RECORDS the role. READ FOR ONE THING: a "currently" (or any open period — "since 2019") is held against the role history — an open period at a party whose every recorded row has ENDED establishes nothing current, and is counted no further than the record's end (never past the document's own date; an undated document dates nothing, and the record's end is not lent to it). Only rows an export DECLARED are the record (never a row a model extracted from a document); a record with a withheld row closes nothing. Without it a stale "currently" in a bio reads as current (recency_unchecked_against_record). NOT READ FOR A LENGTH: the reader has a step that takes a length the profile does not state from the role's dates, and it is SWITCHED OFF in the product until its verification passes — no request can turn it on. Sending record_check is still accepted (never a 422 for that), and the block says what it was read for: record_check.length is {consulted: false, why: "switched_off"} beside record_check.open_period {consulted: true}. Nobody is matched on a record's dates (no rests_on_record, no years_recorded): a profile that states no length stays one condition short (duration_unknown).

SearchFacetSpec

One condition of a criterion, in the criterion's own words.

  • key (string, required) — A short lowercase identifier for the condition (product, market, duration); unique in the criterion; not supports / years / read_failed.
  • kind (one ofcondition,duration,recency`, required) —condition: something an experience states.duration: a length of time (min_years).recency:current, orwithin_years`.
  • text (string, required) — The criterion's EXACT words for this condition (whole words of text, occurring once).
  • min_years (number, nullable) — duration: the threshold, written in the facet's words ("20+ years" → 20).
  • current (boolean, default False) — recency: "currently".
  • within_years (number, nullable) — recency: "within the last N years" → N.
  • value (string, nullable) — condition: the ONE vocabulary term it names ("Europe" for "in European markets"). Typing a condition lets the reader refuse a quote that names a broader or another term ("across EMEA", "across Southeast Asia"); untyped, that rests on the model alone. Kept only when it resolves on exactly one axis. Omitted, the reader still types a condition whose own words name one term; a value stated here wins over them.
  • also (list of string, nullable) — condition: the OTHER NAMES YOU ACCEPT for it — what else counts ("espresso machines" for "kitchen appliances"). A profile whose own words for this condition write one of them establishes it by YOUR statement, not by the reading model's knowledge of the trade: the match says so (checked_by: accepted_name+second_call, accepted_name). A name is matched AS A PROFILE STATES IT: the words the reading names as the value must BE the name (a determiner before it and its last word's plural aside; never up to a spelling). A narrower or a longer phrase is another name, yours to add as it stands ("commercial espresso machines", "espresso machine filters", "espresso machines and grinders" — stated_instead lists what the profiles state). The name SETTLES the condition only where its sentence states it as the person's own work in a form the reader knows ("sold …", "was responsible for …", "her portfolio centred on …"); in any other sentence it stands as a vote the second call decides (such a match says checked_by: second_call and carries no accepted_name), and it is not read where the sentence takes it out or denies it ("excluded …", "unlike …", "with the exception of …", "never worked on …") or writes it as part of another thing ("filters for espresso machines"). Up to 12 names of at most six words. Refused (422): the condition's own words, a denial or preference, a number or a length, words that name nothing, a name another condition states, and — on a typed condition — a name the vocabulary places outside its value or broader than it (state such a value as value). Yours alone: a model's decomposition never carries one. Part of the criterion's identity — other names are another criterion, read again. The response's stated_instead lists, per condition, what the profiles one condition short STATE for it and how many people: the words to rule on.
  • span (list of integer, nullable) — [start, end) of text in the criterion, as a response gives it. Optional; when sent it must be where the words stand.
  • typed (boolean, nullable) — ECHO of a response's facet — ignored (the reader types it).
  • value_source (string, nullable) — ECHO — ignored.
  • typed_reason (string, nullable) — ECHO — ignored.
  • axis (string, nullable) — ECHO — ignored: the vocabulary axis a typed condition is typed on, as a response gives it (the reader types it again).
  • core (string, nullable) — ECHO — ignored (derived from text).
  • carried (list of string, nullable) — ECHO — ignored.
  • completed_by (string, nullable) — ECHO — ignored.
  • why (string, nullable) — ECHO of a suggested entry — ignored: why the reader could not prove the model's facets covered these words.
  • reading (string, nullable) — ECHO of a suggested entry — ignored: which of two readings of one piece it belongs to (split | whole). Send ONE reading's entries back: the two share a key.

SearchIdeaBudget

What ONE idea search may spend sorting labels it has not sorted before.

  • max_calls (integer, default 20) — Model calls this search may make to sort labels new to the idea (25 labels a call). Labels left unsorted are counted (labels_not_grouped) and sorted by the next call. 0 = stored verdicts only (open to a read-only key).
  • max_seconds (number, nullable) — How long this call may spend sorting (default and ceiling: the deployment's request wall, 40 s).

SearchRecordCheckSpec

Which relation is the structured RECORD of a role.

  • relation (string, required) — A relation the workspace's packs declare that records the role (held_role).
  • party_slot (string, required) — Its entity_ref slot naming the employer or client (company).
  • label_slot (string, nullable) — Optional — its slot holding the record's own NAME for a role (title). It must be the role's own name. Read only by the step that takes a LENGTH from the record's dates (it tells several roles at one employer apart by the title the experience's sentence gives the person) — and that step is SWITCHED OFF (see record_check): accepted, validated, part of the criterion's identity when stated, and not used while it is off. WHAT STATING IT COSTS: the same criterion with and without it is two addresses, each read in full, for the same verdicts — leave it out.

SlotReceiptOut

THE RECEIPT FOR ONE STRING SLOT of a fact.

A categorical slot — a title, a company, a geography — is a string, and the numeric receipt (page / char_start / char_end / byte_verified on the fact) locates a NUMBER. This locates the slot's own surface form: exactly once inside the fact's already-attested sentence / row span (verbatim_page / verbatim_char_start / verbatim_char_end).

byte_verified True means re-reading page from char_start to char_end with limma_region reproduces value up to whitespace runs — the one loosening the rule allows, so a title the page wraps across a line still receipts. TO REDEEM, collapse whitespace runs on the region before comparing (" ".join(text.split()) == value); a plain == is False on a wrapped title with a true receipt. The three offsets are non-None exactly then. reason is None when verified and otherwise one of no_enclosing_receipt (the fact itself has no attested span — nothing proven to look inside), empty_surface, unprintable_surface (the extractor stored a typed value — 2019, 5.0, true — not a printed string; its printed form would have to be invented, so it was NOT looked for and value is its JSON rendering), absent (not printed inside the span), ambiguous (printed two or more times inside it — which one is this slot's cannot be said without guessing) or attestation_error (the page layer raised before the rule could run for this fact — the failure is ours and is logged; it says nothing about whether the fact has a span). Every slot the claim declared is here, whatever its surface. The two-digit floor the numeric rules apply (short_token) never applies here: "VP" and "3M" receipt — but the numeric rule's BOUNDARY does: a digit run that is a ./,-joined piece of a longer printed number ("5" inside "5,000", "27" inside "27.4%") is absent, exactly as the fact-level receipt would refuse it, so the two receipts never disagree about the same bytes. A fragment of a longer word ("VP" inside "SVP", "President" inside "Vice-President", wrapped at a line end or not) is absent for the same reason.

  • value (string, required) — The slot's stored surface form — the string this receipt locates. When reason is unprintable_surface it is the typed value's JSON rendering.
  • page (integer, nullable) — 0-based page index of value in the source's persisted page texts. Non-None exactly when byte_verified is True.
  • char_start (integer, nullable) — Start of value on page, in characters. Non-None exactly when byte_verified is True; re-slice it with limma_region.
  • char_end (integer, nullable) — End (exclusive) of value on page, in characters. Non-None exactly when byte_verified is True.
  • byte_verified (boolean, default False) — True iff the region at (page, char_start, char_end) reproduces value up to whitespace runs — redeem with " ".join(text.split()) == value, never a plain == — located exactly once inside the fact's own attested span.
  • reason (string, nullable) — None when verified; otherwise no_enclosing_receipt (the fact has no attested span to look inside) · empty_surface · unprintable_surface (a typed value whose printed form would have to be invented, so it was not looked for) · absent (not printed inside the span) · ambiguous (printed two or more times inside it).

StateStatement

  • statement_id (string, required) — —
  • attribute (string, required) — —
  • entity (string, required) — —
  • period (string, required) — —
  • value (number, nullable) — —
  • status (string, required) — —
  • confidence (number, default 1.0) — —
  • promotion_path (string, nullable) — —
  • source_id (string, nullable) — —
  • slots (dict) — The slots this statement declares, {slot: value} as written (e.g. {"mentioned_entity": "The Coca-Cola Company"}). A statement with slots is ANOTHER PARTY's figure recorded on this (entity, attribute, period), never the entity's own value there; {} means the entity's own fact. Licenses: telling two rows at one address apart. Does not license reading a slotted value as the entity's.

VerificationLevel

How the answer is justified — the single tier an agent keys off.

The two CERTIFIED tiers are produced by deterministic constructors: a pure function of the claims and the key, with no LLM decision. CORROBORATED is a SOURCED answer cross-checked by two or more independence-tested derivations over disjoint source cells — calibrated and judge-checked, NOT deterministic; it sits between SOURCED and the CERTIFIED tiers in trust. SOURCED is an LLM/narrative answer every clause of which resolves to a cited cell (grounded, but not deterministically reproducible). LLM_REASONED carries ungrounded inferred prose. REFUSED means no grounded answer was formed — see refusal_reason.

(no fields)

Warrant

The epistemic warrant for one answer — certified, sourced, or refused.

  • verification_level ([VerificationLevel](#verificationlevel), required) — —
  • deterministic (boolean, default False) — True iff this answer is a reproducible function of the SUBSTRATE STATE — the gate binds it deterministically from the captured cells, so re-running the gate over the same substrate yields the same value (the CERTIFIED tiers; and the deterministic substrate-insufficient refusal = "retrying will not help"). HONESTY BOUND: this is NOT a claim that the whole pipeline is byte-reproducible — the plan-COMPILE step is a hosted-LLM call that is only near-deterministic at temp-0, so WHICH plan runs can vary run-to-run. What is guaranteed is that once a plan reaches the gate, the CERTIFIED value is a deterministic function of the data, not of the LLM. An agent can treat a deterministic=True result like a database read of the substrate; it should NOT assume the SOURCED/CORROBORATED path is byte-reproducible.
  • citations (list of string) — Statement IDs the answer is grounded in (first-appearance order). LEGACY flat form; prefer structured_citations for byte-true, agent-verifiable provenance.
  • structured_citations (list of [Citation](#citation)) — Byte-true, source-attributed citations. Each names source + label + value + verbatim span + a re-sliceable byte region where the token is unambiguous. Empty when the answer was not built over a document corpus (e.g. the /v1 synthesis path).
  • certified_form (string, nullable) — The CERTIFIED value rendered WITH its byte-earned unit scale ('5,466,312 thousand USD'): what a client should display. Set ONLY on the CERTIFIED tiers and only when the executor stamped an earned unit signature carrying a scale word; the raw value token (the receipt) is never altered. None everywhere else — a bare token makes no scale claim.
  • disambiguation (list of dict, nullable) — When an answer was CAPPED or refused because a coordinate carries COMPETING readings the substrate could not reconcile, this is the machine-actionable choice: [{concept, period, readings: [{value, source_id, table_index, row_index}, ...]}]. An agent reads it, then either narrows with scope and re-asks, or verifies which reading it means — instead of silently acting on one side of an unresolved disagreement. None when nothing was contested.
  • calibrated_error_rate (dict, nullable) — The MEASURED error rate of this answer's trust tier, so an agent can PRICE the risk of acting on a SOURCED answer instead of blindly deferring it: {error_rate, ci_lower, ci_upper, n, measured_on}. measured_on carries the provenance (benchmark/workspace, n, caveat) — the rate NEVER travels as a universal claim. None when no calibration is loaded for this surface (honest absence: we do not invent a rate we did not measure). A workspace should recalibrate on its OWN graded feedback; the shipped default is a benchmark calibration, named in measured_on.
  • demands (list of [DemandCoverage](#demandcoverage)) — WHAT EACH PART OF THE ANSWER ASKED FOR AND GOT, per demand. Read it BESIDE facts: it says whether the fact list was FETCHED by coordinate (lane == "lookup") or RANKED by word overlap (lane == "fallback"), and for the fetched part it states how many matches the store holds against how many were delivered. Empty on paths that never ran retrieval (the deterministic answer path when no retrieval lane is configured, and the legacy /v1 synthesis path) — honest absence, not a claim that nothing was demanded. See DemandCoverage; bound is often False, and that is a property of the store, not a failure.
  • facts (list of [DeliveredFact](#deliveredfact)) — Every relevant fact retrieval found, not only the one the answer used. Empty on paths that do not run retrieval (the deterministic answer path when no retrieval lane is configured, and the legacy /v1 synthesis path), which is honest absence rather than a claim that nothing was relevant. Read coverage alongside it: a non-empty facts with facts_dropped_by_cap > 0 means the agent is holding a ranked PAGE of the relevant set, not the whole of it.
  • derivation (dict, nullable) — THE REASONING DAG, so a derived value is checkable instead of trusted. {value, tier, nodes[], edges[], holes[], assumptions[]} — every node the executor solved, step by step. Why the graph and not a summary: the warrant's tier is the MINIMUM over the answer's dependency closure, so a single weak operand caps the whole answer. nodes[].tier names WHICH one, which is the difference between "discard this answer" and "supply a better source for step s5". On a REFUSAL, holes[] carries the operands that DID bind plus the one that did not — turning "no" into a machine-actionable request. assumptions[] carry typed codes, because an agent cannot branch on a sentence. None when the answer never ran the typed executor (a pure proposer-lane result has no DAG) — honest absence, not an empty graph implying reasoning that did not happen.
  • answer_context (dict, nullable) — The source's own SENTENCE explaining the value, travelling ALONGSIDE it. {step_id, op, quote, spans: [{span_id, source_id, text, block_char_start, block_char_end, header_path, page, char_start, char_end, byte_verified}], spans_total, detail, in_answer: false}. block_char_* are offsets into the containing block and are always present; page/char_start/char_end are page-relative and are non-null ONLY when byte_verified is true. A number and the sentence that explains it are different KINDS of claim: the number can be certified (the gate binds it deterministically from captured cells), the sentence cannot (its selection as the relevant one is proposed, never verified). Grading them separately would mean two verdicts for one answer, which the single tier every consumer keys off cannot express. So the quote ships as CONTEXT: the plan's EMIT names a prose step, its byte-true text rides here, and it is excluded from the answer's dependency closure — the value and tier are byte-identical to the same plan with no context at all. in_answer is always false, stated rather than implied: this quote is NOT part of the evidence the value rests on, and a citation-shaped block next to a CERTIFIED number would otherwise read as if it were. byte_verified follows the same rule as structured_citations — true only when the sentence occurs exactly once in the source's page texts and the resolved slice reproduces it. None when the plan named no context or the document explained nothing. LIMIT — the quote ships only when the EXECUTOR's own result is the one that ships: when a proposer lane wins the arbitration, this field is None even though the plan named a context step and the executor resolved it. The quote was selected to explain the executor's derivation, so it is never attached to a different lane's value.
  • ranking (dict, nullable) — THE ORDER a "which member" answer was picked from. {fn, tier, members_considered, member_set, order: [{rank, label, value, numeric, margin_to_next, margin_to_next_relative}], margins_available, margins_unavailable_reason, decisive, tie_at_top}. A ranking question ("which segment had the lowest net revenue", "which region had the biggest drop") is a DERIVATION whose product is an ORDER; the engine computes that order inside AGGREGATE argmax/argmin and returns one label from it. This field is the order itself — the runner-up, the margins, and how many candidates were compared. tier IS THE RANKING'S OWN, AND IT IS NEVER CERTIFIED. The order asserts more than the answer does — the relative position of every member, and implicitly that these were the members — and the engine cannot verify the membership half (the member list can be the compiler's own enumeration), so the ranking's grade is capped at QUALIFIED whatever the answer's tier. Read this tier, not the warrant's: a CERTIFIED_NUMERIC answer can carry a QUALIFIED ranking, and that is the honest pair, not a contradiction. decisive is STRICT separation (margin_to_next > 0) and nothing more — a "materially ahead" threshold would be an invented prior. The margins are supplied so the AGENT applies whatever materiality its task needs. margins_available is false, with every margin None, when the members do not share one unit signature: a difference across dimensions is not a quantity. None when the answer did not rank (no argopt at the EMIT) or when a proposer lane rather than the executor won the arbitration — honest absence, not an empty order.
  • capability_gap (string, nullable) — The compiler's OWN declaration that the question needs an operation the plan algebra does not have ("inexpressible"). It distinguishes "I cannot ask this question at all" from "I looked and the fact is absent". Those demand opposite responses — the first means rephrase or use a different tool, the second is a data problem. None when the compiler expressed the question fine.
  • shape (string, nullable) — The query's classified shape (LOOKUP / DERIVED / MULTI_HOP / COMPARISON / ENUMERATION / QUALITATIVE / ABSENCE / THRESHOLD / COMPOUND / OUT_OF_SCOPE). Not populated yet — always None today; the field is declared now so the contract is stable and consumers can begin reading it without a breaking change later.
  • shape_confidence (number, nullable) — The router's confidence in shape. None while shape is None.
  • sources_bound (list of string) — The document the executor resolved the answer's coordinates against — normally a single id even when the workspace holds many, and even when retrieval read several (coverage.sources_searched says how many documents were read). Stating it is the point: an agent must be able to see which document its answer was bound to, rather than infer completeness from a confident tier.
  • coverage (dict, nullable) — The SOURCE half of coverage: {sources_in_workspace, sources_answerable, sources_searched}. sources_searched < sources_in_workspace means the answer is drawn from a SAMPLE of the workspace, and the agent should treat completeness claims accordingly. None on paths that never touched a workspace store. The FACT half: facts_scored / facts_delivered / facts_dropped_by_cap / layers. facts_scored is every fact retrieval SCORED, not the pool that survived retrieval's own per-layer caps. facts_dropped_by_layer_cap splits the FIRST truncation stage out of facts_dropped_by_cap, because the two demand different responses: a fact dropped by a layer cap is unreachable at any facts value, while the remainder is fixed by asking for more. Absent — not zero — when no retrieval accounting exists for this answer. The RETRIEVAL-QUALITY half: embedding = {active, model_version, dimension, weight_per_query_token, clauses_scored, compared, no_stored_vector, refused, by_status}. refused > 0 says the prose layer was ranked by TOKENS ALONE because the persisted clause vectors were produced by a different embedding model than this deployment's — a deployment fault whose only other symptom is a similarity of 0.0, which reads identically to "nothing in the prose was relevant". prose_edges_derived — true / false / null — is the state of the clause-to-number join for this workspace. FALSE means every facts[].states_values and facts[].stated_by on this warrant is empty because the join was never computed, NOT because the document holds no such sentence; null means the probe itself could not run and the empty joins mean nothing either way.
  • coordinates (dict, nullable) — THE COORDINATE-BIND SIGNAL: which parts of the question addressed real structure, and which did not. {required, bound, fully_bound, coordinates[], unbound[]}; every entry is an ADDRESS — {step_id, op, concept, period, period_source, status} where status is bound (the executor resolved it), unbound (it tried and holed, with the structural code) or unreached (execution stopped at an earlier hole, so this one was never attempted). WHY AN ADDRESS AND NOT A COUNT: "2 of 3 coordinates bound" is a shrug. "cost of revenue @ 2022 did not bind (E_UNMATCHED_COORDINATE)" NAMES the part of your question that did not come from structure. It is the difference between "I found the FY2022 figure you asked for" and "I found something", which are otherwise the same SOURCED string. WHAT unbound MEANS — AND WHAT IT DOES NOT. It means THIS PART OF THE ANSWER DID NOT COME FROM A STRUCTURAL BIND. It does not mean the answer is missing or wrong — the deterministic leg may have holed while another lane answered correctly — and a non-empty unbound[] is not on its own a reason to re-ask. Supplying the named operand, narrowing scope or splitting the question is therefore the RIGHT move only when the warrant is REFUSED, or when the action you are taking specifically needs the structural bind (a certifiable operand) rather than any correct answer. Treat this field as a PROVENANCE disclosure, and verification_level + calibrated_error_rate as the correctness signal. VALUE-FREE: every field is copied from the value-blind plan grammar plus the executor's structural diagnostic code, so it is safe on a REFUSED warrant — it names what was MISSING without leaking the value that was withheld. That is why it also survives the require_tier withhold: the value is withheld, the addresses are not. fully_bound IS NOT A CORRECTNESS SCORE. It measures whether each coordinate resolved to SOMETHING, never whether it resolved to the RIGHT thing — a wrong bind reads as bound. None when no plan ever executed (the compiler could not express the question — see capability_gap — or the surface never ran the typed executor). Honest absence: an empty coordinate list would falsely read as "this question needed none".
  • licence (dict, nullable) — TARGETED LICENSING — the INDEPENDENT support-check verdict over this answer's own assertions: {applied, targeted_by[], skipped_reason, atoms[], n_atoms, n_checked, n_licensed, n_dropped, origin_counts, value_licensed, degraded, checker_calls, checker_model, atomizer_calls, checks_capped, licensed_text, dropped_notes}. WHY AN AGENT SHOULD READ IT. Every other field on this warrant is the engine's own account of its work. This one is a SECOND opinion, produced by a different model that was shown only (span, claim) — never the question, never the plan, never the engine's reasoning — so it cannot be talked into agreeing by the same context that produced the answer. atoms[].origin says WHY each assertion was or was not checked: STATED (byte-attested against the source) and COMPUTED (bound by the deterministic gate) are never sent to the checker, because they are already verified by stronger means; INFERRED atoms are; IMPORTED atoms have no span at all and are never licensed. degraded: true means the value did NOT survive and this warrant is a REFUSAL that would otherwise have been an answer — read refusal_reason: unlicensed_claim beside it. applied: false with a skipped_reason is the honest "this answer was not targeted", not a pass. None by default, and that is the normal state. The gate ships behind EPISTEM_ANSWER_LICENCE=1; with the flag unset this field is None on every answer and nothing in the answer path consults it. None therefore means "no licence opinion exists", never "licensed".
  • fingerprint (string, nullable) — SHA-256 over the canonical synthesis result — stable across renderings.
  • answer_id (string, nullable) — THE HANDLE ON THIS ANSWER'S RECORD. The engine persists what it answered (question, plan hash + canonical plan, substrate fingerprint, tier, value, delivered fact handles, timestamp) so an answer is attributable after the fact and a wrong one has something to go back to. This is the id of that record; an agent keeps it and can later fetch the record from GET /api/workspaces/{workspace_id}/answers/{answer_id}. None on every path that does not record answers (offline use, the legacy /v1 synthesis path) AND whenever the write failed — which is deliberate: the id is a promise that a record exists, so it is set only once one does. Its absence never degrades the answer; nothing else in this warrant depends on it.
  • refusal_reason ([RefusalReason](#refusalreason), nullable) — —
  • refusal_missing_keys (list of string) — When REFUSED: the specific keys the agent would need to supply, as attribute@entity@period (* for an unconstrained axis). EMPTY IS MEANINGFUL AND MUST BE READ WITH refusal_reason. Under compile_failed there are no missing keys BECAUSE no lookup was ever attempted — the list is empty for the opposite reason it is empty under insufficient_evidence (where it means the executor named no hole).
  • compile_error (string, nullable) — The compiler's OWN last error, when the refusal happened because no plan was ever compiled (refusal_reason: compile_failed): either the LLM call failing ("compile call errored: ...") or the static plan verifier rejecting every attempt. It is how a broken deployment or an unparseable question is told apart from a corpus that lacks the fact: the next action is to fix the deployment or rephrase, not to ingest. Distinct from capability_gap, which is the compiler SUCCEEDING at reading the question and declaring the algebra cannot express it. None whenever a plan compiled (including every answered question).
  • tokens_in (integer, default 0) — —
  • tokens_out (integer, default 0) — —
  • decisions (list of dict) — FLIGHT RECORDER: the per-lane decision ledger the answer machine recorded — what each proposer lane proposed, which fold rule decided, and the organ-harvest record. Read-only telemetry: empty for every path that does not run the answer machine.
  • organs (list of string) — Organ-fired markers harvested from the final result's own records — names the mechanisms that shaped this answer. Empty when no organ stamped the result (or on paths that do not run the answer machine).
  • calls_saved (dict of integer) — Per-seam count of LLM work this answer did NOT have to do — {"synonym_memo": 4, "numeric_depth_veto": 2}. The counterpart to the latency block, which can only ever count calls that HAPPENED. numeric_depth_veto counts cold re-extraction FETCHES skipped (each is 1..3 extractor calls, so it is a floor) on questions where the numeric fold was going to stand its candidate down anyway. Empty when nothing was saved.
  • pending_deepening (boolean, default False) — FAST depth only: True when a cold demand re-extraction was DEFERRED to the background heal daemon instead of run synchronously. The current answer did not use that (still-cold) evidence; re-asking the SAME question later may warm-hit once the daemon has healed. False on every synchronous / non-FAST path.
  • value (string, nullable) — The TYPED answer value, or None when the engine produced only prose. A number, a boolean rendered as "true"/"false", or a short name. NEVER a sentence: value_kind == "prose" with value is None is the honest report that the engine answered in English and no typed value could be recovered. No number is ever scraped out of a sentence to fill this field.
  • value_kind (string, nullable) — What value IS — quantity | boolean | text | none | prose. An agent reads this BEFORE value so it never has to guess from the shape.
  • prose (string, nullable) — THE ANSWER WHEN IT IS A PASSAGE, not a value. The value classifier is deliberately strict — a value is a value only when the WHOLE string is one — so a clause bound as an answer classifies as PROSE and leaves value null. So: when value_kind == "prose", the answer is HERE. value stays null on purpose — an agent that scraped a number out of a sentence would be repeating the exact defect the strict classifier exists to prevent — and the passage stays readable, with its citation carrying the byte range it came from.
  • verdict (string, nullable) — What the root goal concluded: ANSWER | REFUSE | CONFLICT. Distinct from verification_level, which says how WELL supported it is — a weakly supported ANSWER and a REFUSE are different outcomes, not different confidences.
  • citation_check (dict, nullable) — The answer-time byte check over structured_citations: {kept, stripped, downgraded, stripped_cites: [{doc, quote, doc_known, quote_found}]}. Every quote is re-checked against the text of the document it NAMES, at answer time. A quote that is not found verbatim is REMOVED from structured_citations and recorded here. downgraded: true means the answer still shipped but with thinner provenance than it claimed — deliberately not a refusal, because refusing on a failed check was measured to cost more coverage than it buys.
  • absence (dict, nullable) — THE PRESENCE TRICHOTOMY: whether the thing asked for is here, is provably not here, or could not be established either way — with the denominator that licenses the claim. {state, open_gaps[], gap_causes{...}, searched{...}, would_have_counted{...}}. state is one of: * present — the root goal concluded ANSWER or CONFLICT. Copied from verdict, never re-decided here. * provably_absent — the ONLY state that licenses "it is not there". Earned by having no open gap at all: a demand was expressed, nothing was dropped by any cap, no source was unreadable / missing / truncated, every source in the workspace was searched, every byte of every searched source is accounted for, and nothing the engine examined discharges the demand. * absent_with_open_gaps — the DEFAULT, and the honest answer whenever any of those fails. open_gaps[] names which, from a closed vocabulary: never_compiled (the engine never reached the evidence) · truncated_by_cap (scored evidence was thrown away before delivery) · source_unreadable · workspace_not_fully_searched · undisposed_spans (text no layer examined) · unparseable (the denominator itself could not be established) · demand_matched_not_answered (matching evidence IS in the agent's hands and no value formed — whatever that is, it is not an absent fact). gap_causes is {code: [reason, ...]} — WHICH of the several conditions behind each code actually fired, in English, with the numbers. The codes above stay a closed set to branch on; this is what makes a gap actionable rather than merely honest. One code can stand for several conditions: unparseable fires on a coverage counter that would not cast, a span with no byte offsets, an unverifiable passage scan, a missing inventory, and a question that expressed no demand — five conditions, five different fixes. A refusal reason alone does not establish that a fact is absent: a refusal can be reached without the engine having searched everything. absence is the field that can say so — only state: provably_absent licenses "it is not there". searched is the DENOMINATOR, per source: bytes_total (the source's own extracted text), spans_disposed + bytes_disposed (what a retrieval layer actually examined — the merged union of the stored clause spans and, when its window arithmetic verifies against the layer's own counter, the passage layer's full tiling), bytes_undisposed + undisposed_spans (text NOTHING examined, where the answer could still be), and dispositions (the clause classes those spans were typed as, with the unclassified sentinel counted as its own disposition and never folded into the typed ones). Alongside them, the reused sources_searched / sources_in_workspace / facts_scored / facts_dropped_by_cap — READ off coverage, not recomputed. would_have_counted is what the engine was looking for, LABELLED with where it came from: plan_coordinates (the compiler's addresses, when a plan compiled), retrieval_demands (the demands record, the fallback that survives a compile failure) or none. None means the block could not be computed — never present and never provably_absent. A consumer that finds no absence block knows only that this answer carries no absence accounting.