Skip to content

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).

EndpointVerb · answer kindBody
POST /sys/query/entitiesQuery · entity set{ context, body: { query: QuerySpec, opts?: ReadOpts } }
POST /sys/query/aggregateQuery · grouped rows{ over: QuerySpec, bucket?, groupBy?, measures, having?ᵣ }
POST /sys/query/seriesQuery · time points{ subject, trait, range, eval } (ADR-0015 vocabulary)
POST /sys/subscribe/viewSubscribe · 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)
LeafShapeValidated 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:

DeliveryContent typeContract
Page (default)application/jsonbounded by limit + workload-class cap; keyset cursor
Row streamapplication/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.streamArrow schema derived from the ontology’s parameterized kinds (numeric widths, geo classes); un-projected traits fall back to one JSON column
Export jobopts: {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 diffstext/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 nowBIND-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.