Query Surface
Query Surface — QuerySpec v0
Status: grammar ratified 2026-07-03 (ADR-0028); route surface ratified 2026-07-04 (ADR-0033). QuerySpec is v0 — one grammar that evolves compatibly; the earlier “v2/v3” workstream labels are retired. Scope: the wire grammar clients speak and both implementers (kernel, TS FixtureKernelClient) execute identically. Pipeline semantics — stages, tiers, barriers, routing — live in the Data Plane; this page never restates them. The surface workstream’s grammar decisions (D6/D14–D16 in Surface System) resolve here.
QuerySpec is the wire surface; the QueryIR never serializes publicly — BIND
compiles QuerySpec into the kernel-internal plan language. (The LLD’s IR-taxonomy
calls the QueryIR IR-2; ADR-0028 uses that older label. In code the QueryIR has
two forms: QueryPlan after BIND, SecuredQueryPlan after SECURE.) The grammar is ratified once;
blocks the pipeline does not yet execute are rejected at BIND with typed
NOT_YET_SERVING errors naming their gate (staging table at the bottom).
The doorways
One pipeline; Query has three answer-kind doors (one-shot pull). The diff-stream door is the Subscribe verb (standing push), not a Query answer-kind — pull-vs-push is the verb boundary (ADR-0033).
| Endpoint | Verb · answer kind | Body |
|---|---|---|
POST /sys/query/entities | Query · entity set | { context, body: { query: QuerySpec, opts?: ReadOpts } } |
POST /sys/query/aggregate ᵣ | Query · grouped rows | { over: QuerySpec, bucket?, groupBy?, measures, having?ᵣ } |
POST /sys/query/series ᵣ | Query · time points | { subject, trait, range, eval } (ADR-0015 vocabulary) |
POST /sys/subscribe/view ᵣ | Subscribe · diff stream | { view: QuerySpec, resumeToken?, options: { debounce? } } |
Routes follow the one rule POST /sys/<verb>/<name> (lowercase-kebab, name in
the path), ratified in ADR-0033: the six
verbs organize the surface, and kernel introspection (the error catalog, schema/
ontology, capabilities) lives under GET /sys/meta/<topic>. ADR-0028’s /v0/*
and PascalCase spellings were shorthand.
Context (tenant, universe, branch, actor, capability) rides the command’s
context object (house syscall convention; the transport boundary owns trace
correlation), never the spec. The condition tree is the shared organ: aggregate.over and subscribe’s
view embed full QuerySpecs, so guards and dials flow into every answer kind.
QuerySpec
QuerySpec { from?: { kind: Kind | Kind[] } | { iface: Kind } // interface ⇒ conforming-kind expansion // optional only when `ids` is present ids?: EntityId[] // point-read: address entities directly where?: Cond // the condition tree (below) select?: { traits?: Kind[] | "*", links?: boolean, evidence?: boolean } // evidence ⇒ elevated capability time?: { latest: true } // default — asOf | window | branch staged | { asOf: OrderToken | ISO } | { window: {from, to} } | { branch: BranchId } graph?: { traverse: { steps: [{ link?: Kind, family?: Kind, direction: "out"|"in"|"both" }], depth: number } } | { trace: { path: PathId, direction: "up"|"down", until?: Cond, maxDepth?: number } } sort?: [ { trait, field?, dir } | { distance: {trait, from: GeoJSON} }ᵣ | { score: "desc" }ᵣ ] limit?: number cursor?: string // opaque keyset cursor: sort-position + spec-shape hash; never OFFSET}ReadOpts { barrier?: { afterToken: opaque-receipt-string } | { freshness: Duration }, // ^ the ordering-token string an Act returned, echoed verbatim — // token formats are adapter-owned; the kernel never parses them onTimeout?: "serve" | "fail", // default serve — verdict in explain explain?: "none" | "execution" | "data" }Notes: absent sort means the deterministic default order (ascending entityId —
never random); graph.trace references declared ontology paths (PathDecl) and
reports frontier truncation honestly in the envelope; older clients are unaffected —
the flat where array parses as {all: […]} forever.
A query is anchored by from (a kind/interface scope) or by ids (a direct
point-read), and may carry both — ids then intersects the kind scope. ids alone
is unconstrained by kind and lowers to an IdIn leaf both adapters execute. where
and sort still require from, because their trait references resolve against the
concrete kinds; a lone ids query rejects them at BIND.
The condition tree
Cond = { all: Cond[] } | { any: Cond[] } | { not: Cond } | Leaf (depth ≤ 8 at BIND)| Leaf | Shape | Validated against |
|---|---|---|
| trait-op | { trait, field?, op, value } | op × parameterized kind matrix |
| hasTrait | { hasTrait: Kind } | trait declared on kind/iface |
| hasLink | { hasLink: { kind, direction } } | link declared (§27) |
| settledness | { settledness: { trait?, is: "settled"|"contested"|"stale"|"blind" } } | always legal — trust is queryable |
| geo ᵣ | { geo: { trait, op: "within"|"intersects"|"near", geom: GeoJSON, distance? } } | geo(...) kinds |
| text ᵣ | { text: { query, traits?, mode?: "match"|"phrase"|"prefix" } } | text-indexed traits |
| similar ᵣ | { similar: { index, to: vector|{entity}, k, minScore? } } | SERVING embedding index |
The nine trait ops — eq neq in gt gte lt lte within contains — carry over
unchanged; within remains the range operator (spatial lives in the geo leaf, so the
namespaces never collide). Binding values ($dials.*, $session.*) are resolved
upstream by the Surface runtime; the seam sees literals only.
The result envelope
Every trait value is a ResolvedValue — never a naked scalar:
{ "items": [ { "ref": {...}, "kind": "…", "traits": { "…": { "value": …, "settledness": "contested", "warnings": ["…"], "derivedFrom": ["stmt:…"], "at": "…" } } } ], "pageInfo": { "cursor": "…", "hasMore": true }, "explain": { "visibleThrough": {"cell":"…","seq":…}, "barrier": "satisfied|timedOut,served", "frontier": "truncated-at-…|null" } }Delivery matrix
One streaming execution (leaves produce row streams; nothing is collected), five
deliveries, negotiated by Accept / opts.mode:
| Delivery | Content type | Contract |
|---|---|---|
| Page (default) | application/json | bounded by limit + workload-class cap; keyset cursor |
| Row stream | application/x-ndjson | {t:item} frames; terminal {t:summary} frame carries count · watermark · truncation · spend — a stream without its summary is known-incomplete; {t:error} may terminate |
| Columnar stream ᵣ | application/vnd.apache.arrow.stream | Arrow schema derived from the ontology’s parameterized kinds (numeric widths, geo classes); un-projected traits fall back to one JSON column |
| Export job | opts: {mode: "export", format: "ndjson"|"parquet"ᵣ} | ACCOUNT converts the plan to a worker job streaming to the blob port; output = registry artifact + attestation; caller gets job id → signed URL |
| Standing diffs | text/event-stream (subscribe) | snapshot frames, then ordered view-membership diffs enter|update|leave stamped with OrderTokens; resume tokens over a bounded ring buffer; resync when unresumable; heartbeats carry visibleThrough. Never raw entity CDC for clients to re-filter. POST-SSE at Lean; WebSocket is the P26 graduation |
Staging
| Executes now | BIND-rejected (NOT_YET_SERVING), gate |
|---|---|
from kind/kinds/iface · ids point-read · tree with trait/hasTrait/hasLink/settledness leaves · select resolved · time.latest · graph.traverse+trace · sort/limit/cursor · afterToken barriers · aggregate five measures + groupBy + bucket · series windows · subscribe enter/update/leave + debounce · JSON + NDJSON · export(ndjson) | geo — spatial read-model decision (generated column vs sidecar) · text — tsvector wiring · similar — first real embedding corpus (data-plane §10.5) · `time.asOf |
ᵣ = ratified grammar, reserved execution.
Build note (2026-07-03): the “executes now” column lands rung by rung —
/sys/query/entities (filters, sort, cursor, barriers, envelope) is live;
graph, aggregate, series, subscribe, and the streaming deliveries follow per
the PR ladder in the v0 implementation plan and BIND-reject until their rung
merges.
Conformance
The strict mirror is a machine check: one canonical fixture realm, one
language-neutral directory of (queryspec.json, expected-result.json) golden pairs —
covering every executable block and every BIND rejection class — executed by the
kernel against seeded Postgres and by the TS FixtureKernelClient in vitest.
Ratifying a grammar extension means adding its golden pairs. Drift fails the
laggard’s CI.