Skip to content

ADR-0028: QuerySpec is the wire surface; IR-2 stays kernel-internal

ADR-0028: QuerySpec is the wire surface; IR-2 stays kernel-internal

Status: accepted (2026-07-03)

Number note: 0016–0020 were taken by parallel workstreams that merged to main while this one was in flight (main runs through 0027); this ADR takes 0028 to avoid the ADR-number collision the Book keeps hitting. (In code the type name is QueryPlan / SecuredQueryPlan; “IR-2” is the LLD taxonomy label the title preserves.)

The data plane locked one Query IR, many surfaces (data-plane §9, DP-1) and the surface workstream ratified a concrete client grammar — QuerySpec v2 with a closed nine-operator where list — under an explicit renegotiation clause: “when kernel IR-2 lands, the ref shape is renegotiated against what the SDK actually emits” (surface-system D6/D14). Building the kernel query pipeline forces the renegotiation: what serializes on the wire, how the grammar grows to cover the full IR-2 node set (graph, spatial, text, vector, temporal, aggregate, series, standing), and how “every implementer executes exactly this” stays true once there are two implementers.

QuerySpec is the wire surface; IR-2 never serializes publicly. The seam’s endpoints accept QuerySpec — the shape the surface runtime already speaks — and stage 1 of the pipeline (BIND) is the QuerySpec→IR-2 compiler. The IR remains a closed, kernel-owned plan language free to evolve until its own graduation events (data-plane §10.1); clients never construct plans. Rejected: serialized IR as the wire format (freezes the IR before OPTIMIZE/EXECUTE have exercised it; puts a compiler in every client); GraphQL as the core API (its resolver execution model invites per-field resolution and middleware policy — the post-hoc enforcement DP-4 bans; its open composition is the opposite of a closed install-validated grammar; it remains a later generated projection, per §4.1); dual raw-IR endpoints (drift).

The where grammar is a recursive condition tree with a closed leaf set. Cond = {all: Cond[]} | {any: Cond[]} | {not: Cond} | Leaf, depth-capped at BIND. Leaves: trait-operator (the ratified nine ops; within remains the range operator), hasTrait, hasLink, settledness (the Trust Machine is filterable, per canon “queryable, never hidden”), geo (within | intersects | near, GeoJSON), text (match | phrase | prefix), similar (named embedding index, k, minScore — policy filters the universe before retrieval). Every leaf validates against the trait’s parameterized kind at BIND, mirroring install-time validation. QuerySpec v2’s flat where array remains valid forever as {all: […]} sugar. Grammar precedent: Foundry’s ontology filter trees; the leaf-type (not op-overload) encoding is ours, because parameterized kinds give leaves better validation targets.

Four typed doorways, one pipeline. POST /v0/query, /v0/aggregate, /v0/series, /v0/subscribe — split by answer kind (entity set · grouped rows · time points · diff stream), which is tier semantics, barrier coherence, and budget class made visible (PR-7’s doctrine at the wire). All four enter the same BIND → SECURE → ROUTE → OPTIMIZE → EXECUTE → ACCOUNT pipeline; the condition tree is the shared organ (aggregate.over and series subject-selection embed full QuerySpecs). Recurring history×entity composites are declared derivations/materializations (DP-10, ADR-0015), never a fifth endpoint. With /v0/series absorbing read_series and graph absorbing the trace syscall and CLI graph find, no read path outside the seam survives — PR-2 becomes literally true in code.

Delivery is a content-negotiated matrix over one streaming execution. application/json (bounded page; keyset cursor = opaque sort-position + spec-shape hash, never OFFSET) · application/x-ndjson (row frames flowing Postgres→envelope→socket at O(row) memory; an in-band terminal summary frame carries count, watermark, truncation, and spend — a stream without its summary is detectably incomplete) · application/vnd.apache.arrow.stream (columnar; Arrow schemas derived from the ontology’s parameterized kinds, not sampled) · export mode (ACCOUNT converts the plan to a worker job streaming NDJSON/Parquet to the blob port; output is a registry artifact with an attestation) · SSE for subscribe (snapshot-then-ordered-view-diffs: enter | update | leave stamped with OrderTokens, resume tokens over a bounded ring buffer, resync when unresumable, heartbeats carrying visibleThrough; POST-SSE at Lean, WebSocket is the P26 Scale graduation). Diffs are view-membership deltas, never raw entity CDC for clients to re-filter.

The strict mirror is enforced by a shared conformance suite, not doctrine. One canonical fixture realm; one language-neutral directory of (queryspec.json, expected-result.json) golden pairs covering every executable grammar block and every BIND rejection class; the kernel runs them against seeded Postgres, the TS FixtureKernelClient runs the identical files — drift fails the laggard’s CI. Ratifying a grammar extension means adding its golden pairs.

The grammar is ratified once; execution arrives in stages. Blocks the pipeline does not yet execute are rejected at BIND with a typed NOT_YET_SERVING error naming the gate — never silently ignored, never approximated. v0 executes: from (kind | kinds | interface), the tree with trait/hasTrait/hasLink/settledness leaves, select (resolved-only; evidence deny-all until the capability story), time.latest, graph.traverse/graph.trace (named ontology paths, until, frontier honesty), sort/limit/cursor, afterToken barriers end-to-end, aggregate (count/sum/avg/min/max, groupBy, bucket), series windows, subscribe (enter/update/leave + debounce). Deferred behind named gates: geo (spatial read-model decision), text, similar, time.asOf|window|branch on the query door, percentiles/having/countDistinct, Compare, the §24.1 pattern grammar beyond debounce.

Consequences: the surface team’s D6/D14 renegotiation clause is resolved — their grammar and this contract are now the same document set (grammar reference: architecture/query-surface.md; pipeline semantics: architecture/data-plane.md); sdkgen, GraphQL, and MCP tool manifests all project from this wire contract; every future grammar change is a QuerySpec extension ratified against this ADR’s staging discipline plus conformance pairs; and the kernel accepts the maintenance cost of two closed vocabularies (wire grammar above, leaf-plan port vocabulary below) as the price of adapter-side translation with no SQL in the kernel.