The Frame-QL Manual
The formal query language of Columna, by datumwise
Frame-QL Second Edition — a mechanically-reconciled continuation of the Frame-QL Manual, First Edition (which it supersedes). This edition preserves the First Edition’s text and syncs it to shipped reality through columna-core 0.14.0 (wire contract "2"): the canonical column identity of WP-NAME-1 (0.14.0), the composite input anchor of WP-GRAIN-1 (0.13.4), the minted and tombstoned reason codes, and the version stamps — each change flagged in place with a ▸ Second-Edition sync note and ledgered below. The First Edition was itself a renamed, lightly-revised continuation of the Coframe-QL Manual, Third Edition (which it superseded): it renamed that text to Columna/Frame-QL/Manifold and reconciled it to the column-foundation redesign (ADR-031), the edition split it then carried (since retired — topology record §17.5), and the shipped envelope grammar (ADR-035). It inherits the v4 operator model (ADR-009…015), the V/M/B anchor vocabulary (ADR-024), the anchor-ascription rule, family-aware scans, macro bindings, EXPLAIN, and the (anchor, universe) grain (ADR-025…028). The envelope — SELECT <series> [AS <alias>], … AT {anchor} with its optional FROM, WHERE, HAVING, ORDER BY, LIMIT … PER, and WITH clauses, where @ {…} is the input-anchor marker universally and AT {…} is the sole output-grain declaration — is the language, and this edition documents it as implemented (columna-core 0.18.1). The pre-launch shipped surface was a fragment of it — the terse cols @ anchor form, where a trailing @ spelled the output anchor; that fragment is retired (its two @s meant opposite things) and preserved for lineage in Appendix D. The fourth outcome, named inform under ADR-020’s inform-and-serve doctrine, ships on the wire as the refuse mood; this edition uses refuse throughout (Chapter 7). The §2c universe law (structural universe resolution; one expression, one universe; cross_universe), the corrected B3 basis rule (the basis fixes which points the frame expects; a measure’s fill rule Φ fixes what an absence denotes — columna#148), and the shipped reason codes are reconciled into the semantics they govern.
Currency (2026-08-22). This edition is current for columna 0.18.1 · columna-core 0.18.1 ·
columna-server 0.11.1, wire contract_version "4". The "2" → "3" bump changed the
installation catalog (list_manifolds, which became a governed publication lineage catalog) and
nothing this manual documents; CONTRACT_VERSION is global, so query / check / EXPLAIN /
describe merely report "3" — no Frame-QL syntax, analytical behavior, mood, disclosure or reason
code moved with it. The dated ▸ notes below record when each earlier change entered; they are
history and are not restamped.
About the examples (stated once): every FrameQL example in this manual is checked against the shipped
parser (columna_core.envelope.parse_statement, columna-core 0.18.1) by docs/tools/check_manual_frameql.py,
which rides the manuals merge as a standing test — the manual can never document syntax its own parser
rejects. A code block fenced frameql-illformed is deliberately ill-formed: it shows a syntax the
parser rejects (a [SCHEDULED]/[ROADMAP] surface not yet shipped, or a teaching non-example), and the
check asserts it does not parse. Every other FrameQL block parses clean (it may still refuse or
clarify at plan time — that is semantics, which the four moods report, not grammar).
Preface
The Frame is the query, not operational instruction. Frame-QL takes its name from what remains when you take the Data out of a DataFrame: the Frame — a collection of column expressions on one anchor, and still everything. In Columna a column is an expression of what a thing is, not a step in a process, and a Frame-QL statement is a declared assembly of such columns at one stated grain — never a sequence of operations to perform. The writer says what the answer is — which series, at which anchor, under which conditions — and the framework decides how to realize it. This is the origin law of the language, and everything below is its consequence: the statement names a frame; the planner assembles the frame and owns the envelope’s clauses; the engine computes the series, envelope-blind. Nothing in the surface syntax describes an operation to run.
Frame-QL is the formal query language of Columna. It is the language compilers compile, agents emit, integrity certificates are stated against, and external systems integrate with. Every query against a Manifold — whether typed by a user, generated by the Columna agent, or produced by sugar-expansion from a higher-level surface — ultimately becomes a Frame-QL expression that the framework parses, typechecks, plans, and executes.
This manual is the reference for that language. It is intended for analytics engineers writing queries directly, developers building tools that integrate with Columna, and anyone who wants to understand precisely what the framework will and will not accept.
A note on scope. This manual specifies the language: its grammar, its semantics, its type rules, and the outcomes it produces — every query returns a (result, annotation) pair, and this manual specifies when the result is served, when it is served with disclosures, and in which narrow cases no result can be produced. It is not a tutorial on Columna concepts — for that, see the Columna Framework manual and the companion Columna Reference Manual. This manual presumes familiarity with the framework’s concepts: columns as functions from anchors to values; the three anchors a column carries — the V-anchor (its domain), the M-anchor (its missingness mechanism), and, per family, the B-anchor (the axes its reducer is blocked from rolling up across); the family set, default family, and the fertile/mule distinction; degeneracy and coverage; defining boundaries; reducers and the three anchor-actions crossed with the order axis; and the four-state integrity certificate. Terms are used here as they are defined there.
The grammar is given formally in the companion Frame-QL BNF Grammar; this manual covers semantics and usage. Where this manual and the BNF differ, the BNF is authoritative on syntax and this manual on meaning. A separate theoretical authority supplies the analytical concepts used to explain the language. The stack, stated once: the BNF is authoritative on formal syntax; this manual on shipped semantics, canonical form, operators, outcomes, reason codes, implementation terminology, and version-specific behaviour; the Theory of Data (v6.1, below) on the analytical concepts and laws those distinctions exist to serve. What the parser accepts is a BNF question; what a shipped construct means is a question for this manual; why the language draws a line where it does is a question for the Theory. This manual is not Theory-of-Data documentation and does not become it.
Revision note (Frame-QL Second Edition) — the mechanical sync
The Second Edition changes no doctrine. It is a mechanical sync of the First Edition’s text to the shipped package through columna-core 0.14.0 / 0.13.4 — names, pins, reason codes, and version stamps brought level with what the parser and planner actually do. The rule the desk keeps for its own transcripts is now the manual’s: every FrameQL example is verified against the running parser, not written from memory (the standing self-check of docs/tools/check_manual_frameql.py, run against the shipped columna-core — 0.18.1 at this revision). Each change is flagged in place with a ▸ Second-Edition sync note. The ledger:
- Column identity is the canonical expression, not a mechanical default (WP-NAME-1, 0.14.0; wire contract
"1"→"2"). The<reducer>_<measure>default (sum_revenue) and the dot-to-underscore mangle (revenue_sum) are retired — both were invented names, and the input anchor they dropped is half a pinned reduction’s denotation (the Two Anchors law). An unaliased series is now keyed by its canonical expression, verbatim:avg(revenue @ {day})keys asavg(revenue @ {day})(the pin visible in the key), a bare measure asrevenue, member access asrevenue.sum(dotted, unmangled).EXPLAINemits no redundantX AS X. Consumers needing a stable handle key on anASalias, which is author-owned and changes under no future rule. The bump carries exactly one thing — the default key of an unchanged utterance; no value, mood, disclosure, or reason code moves. (Chapters 1.6, 2.7; Appendix D.) - The composite input anchor: a product grain is a first-class pin (WP-GRAIN-1, 0.13.4; no wire change, contract stays
"1"at that release — a minted reason routes by outcome). An inline reduction’s input anchor may pin a product of levels —avg(revenue @ {store*product*cal.month})— not just one. Two reason codes are minted inside the existing vocabulary:pin_coarser_than_output(REFUSE, Law 1 — a pin level coarser than the output grain cannot resolve at a finer output) andredundant_pin(CLARIFY, Law 2 — two cross-comparable pin levels fix one axis, not two). Law 4 generalizes the immaterial two-stage-statistic provenance note (still a serve). One corner is rowed out: a composite pin whose product includes a faced output coordinate meets the G4 chain guard and refuseschained_crossing— a named refusal, never a silent number. (Chapters 2.3, 2.7; Example 6.16; Chapters 5.6, 7.3.) co_anchor_ambiguousis tombstoned (retired 2026-07-16 under the §2c expression law; a retirement-pin test asserts it is never emitted). The First Edition named it the “multi-input case” of the unpinned-reduction clarify; the shipped planner emitsinput_anchor_ambiguousthere (one reason per contested dimension — OF-1), and the cross-universe rate the reason originally named is now thecross_universecategory error (Chapter 2.5), not a clarify. The spelling is kept as a dated tombstone so old transcripts stay interpretable — vocabularies grow by rule and shrink by tombstone, never silently. (Chapters 2.3, 7.3.)- Version stamps. The Second Edition sync documented columna-core 0.14.0 as implemented (preface, harness note, Chapters 2.8–2.9, Appendix B) and cited wire contract
"2". The retired terse@-fragment (Appendix D) and the grow-by-ruling surfaces are unchanged. (▸ currency update, 2026-08-22: those current-state stamps now read columna-core 0.16.2 and contract"3"— see Currency above. This ledger row is left as the record of what the Second-Edition sync did; the sync itself is not restamped.) - The RELATE face triad completed (0.12.0; scope amendment to phase (a), ratified 2026-07-31 — shipped-reality drift, the same class as the version stamps). The First Edition documented
touchonly, withassign/allocdeclared-but-deferred; 0.12.0 shipped the full triad and all three now execute. Chapter 5.6 now documents each face as the engine emits it:touch(over-count, disclose),assign(ASSIGN BY … ORDER MIN|MAX, single-count reconciling to the grand total, shadowmemberships_unrepresented, disclose), andalloc(ALLOC BY …, weighted split reconciling to the cent, the reconciliation badge{crossed_total, base_total, delta, tolerance, status}), plus the fail-closed per-scheme adjudication, the G5 anchor law (a distinct-class measure refuses at every face), and the G4 chain guard. (Wire additive;contract_versionstayed"1"at that release.) - Wire counters
executed/fetches_delta(additive to contract"2", no doctrine). The annotation carries two server-surface execution counters —executedandfetches_delta— emitted on both the plan (EXPLAIN) and run paths. They are a server-surface detail, not a language feature; they move no value, mood, disclosure, or reason code, and grow no wire chapter here.
Revision note (Frame-QL First Edition)
This edition is the vetted Third-Edition text, renamed and reconciled. The reconciliations are small and are flagged in place with a ▸ Frame-QL revision note. The ledger:
- Many-to-many aggregate-across (fan-out) is a clarification, not a served disclosure (Chapters 5.6, 7.3, 7.4). Under the column-foundation (ADR-031) the engine transports along functional edges and never joins; a non-functional (M:N) edge is therefore not a traversable path, and the membership aggregation the Third Edition served is not expressible without a declared resolution. The unmet precondition is reported, statically and before execution, as a clarification naming the same three remedies. This replaces “serve membership with overlap disclosure” for the undeclared case.
- The inform-and-serve boundary is the planner/engine boundary (Chapters 5, 7.2). Everything statically catchable — ambiguity, unknown operator, unknown column, type mismatch, non-traversable edge, governance/access — is decided by the planner, before resolution, and may yield a clarify/refuse with no result. Analytical risk that survives to the column engine is served-and-disclosed, never withheld. A B-anchor crossing is detected at the planner (it attaches the critical disclosure) and may be downgraded to a route-around at the engine (Chapter 5.3); the number is always served. (▸ shipped-law reconciliation, 2026-08-20, ADR-036: the last clause no longer holds for the structurally prohibited reduction. A reducer travelling a lineage it is declared
BLOCKEDalong is refused at the planner, before any number exists — see item 7 below and Chapter 5.3. The boundary itself is unchanged: everything statically catchable is still decided by the planner, and analytical risk surviving to the engine is still served-and-disclosed.) - Mule recompute is recompute-from-base at the output grain where no fertile decomposition exists (Chapter 7.4, Appendix A) — stated explicitly, consistent with the Third Edition.
- Universe depth. The
(anchor, universe)grain is unchanged at the language surface; the basis-driven empty-bucket rule is corrected (Chapter 1.5, columna#148) — the basis still fixes which points the frame expects, but what an absent value means is now the measure’s fill rule Φ, not the universe basis. Beneath the surface, Columna enriches universe with a population-consistency layer (support reconciliation across a universe’s columns; path-independence of a reduced total across anchorings). This is semantics under the syntax, not new syntax. - The envelope is the language; the terse
@-fragment is retired (Preface; Chapters 1–4; Appendix D). The pre-launch surface spelled the output anchor with a trailing@(aov @ cal.month); the ratified envelope (ADR-035) makes@ {…}the input-anchor marker universally andAT {…}the sole output-grain declaration, and adds theFROM/WHERE/HAVING/ORDER BY/LIMIT … PER/WITHclauses. This edition documents the envelope as implemented; the terse form moves to Appendix D (lineage).FROMis optional — it defaults to the bound Manifold and is required only where a surface holds more than one (Chapter 1.3). - The fourth mood is
refuse, notinform(Chapter 7). The shipped wire’s four moods are serve · disclose · clarify · refuse. The withholding outcome this manual’s Third-Edition lineage called inform (ADR-020’s “inform-and-serve” doctrine) is the wire’srefuse; the doctrine’s spirit — withhold only structurally, disclose analytical risk — is unchanged, and this edition usesrefusethroughout. (The separatecross_universecategory error rides the query-error channel, not a mood — Chapters 2, 7.) - Structurally prohibited reductions refuse; Disclose cannot legalize them (Chapters 5.3, 7.2, 7.3, 7.4). (Ratified 2026-08-20 — ADR-036, superseding ADR-020’s inform-and-serve rule for this case only.) A reduction that travels a lineage its operator is declared
BLOCKEDalong has no lawful reading, so it is refused (blocked_reduction; no values on the wire), in every spelling: written as a declared family member (level.sum), generated by an inline reducer above a lawful sibling (sum(level.last @ {day})), or wrapped in a unary, binary, scalar, scan orDERIVEDcarrier. Family generation creates a new analytical family. It does not create a new operator permission. Applicability stays per operator × lineage — there is no stock/flow/rating type (ADR-031 D5 clarified, not reversed): summing a stock across stores stays lawful, andavg/min/max/countover a stock across time stay lawful, because the author barredsumand barred nothing else. The disclosure codeb_anchor_crossingis tombstoned as a producer and retained, still wired, so archived wires and deposited transcripts still resolve (7.4).CONTRACT_VERSIONstays"3"; the reason string is additive on the existing Refuse channel.
Editions and availability
▸ Currency repair, 2026-08-31. This section previously read “Columna ships in two tiers, and this one manual covers both,” and thirteen constructs below carried an inline [Pro] marker denoting purchasable availability. There is one tier. The marker is retired by topology record §17.5 and the claim by §17.4: no product claim may be created for a delivery form with no factual referent. Each tagged construct has been re-typed below against what the shipped package executes, and two of them — MNAR exclusion (Chapter 7.4) and coverage (Chapters 5.7, 7.4) — turned out to ship today, so the marker had been withholding, in prose, capability the open package already serves.
There is one Columna, and this manual documents it. No construct here is withheld from a reader of the open package, and nothing in this language is priced.
What varies is not edition but availability, and this manual marks it construct by construct in
the idiom the rest of the documentation uses (ADR-034, the honest-hatching rule): [ROADMAP] where
the language defines a construct the engine does not yet run, [SCHEDULED —
The guarantee that replaces the old tagging is stronger than the tagging was. A construct the engine cannot run does not quietly return a number: it refuses at plan time with a named reason, in the declaration dialect, naming the lawful readings that remain (Chapter 7.3). Availability is therefore something the system tells you, not something a manual asks you to trust.
Scope
Columna is stage one of the Statistical Bridge — the governed data objects of an analysis: universes, measures, members, and the frame. The Bridge’s other three stages — the generation of possible evidence, inference from realized evidence to formal targets, and the interpretation of formal results as world-facing claims — are out of scope, by standing rule; their absence is a boundary, not a gap. This is why event and spine are universe types, and always were, while regime, identification, and claim licensing are not: in the Theory-of-Data v5 terminology inherited by this implementation, regime was reserved for the value-generation contract, an inference-layer object — so this manual never calls a basis a regime. That reservation is recorded here as the inherited v5 usage this vocabulary came from, not as a claim about current ToD terminology.
The Theory of Data
The analytical concepts this language serves are stated in The Theory of Data: A Foundation for Analytical Identity, Derivability, and Consistency, Version 6.1 (10.5281/zenodo.22013410, 19 August 2026). That is the edition this manual has been reconciled against. Earlier editions named elsewhere in this text are dated records of what an earlier sync read; they are history and are not restamped.
Two vocabularies meet here, and they are not in conflict. ToD v6.1 §1.2 fixes the theory’s terms: a measure family is the governed analytical family — Revenue; a measure is that family at one anchor, written F@A; and the term member is retired from the core v6 ontology — what Version 5 called a member of a measure is, in v6, simply a measure. Shipped Frame-QL and Columna retain the earlier implementation vocabulary, and this manual is written in it: measure names the governed column (revenue); member is retained implementation vocabulary for the anchored object v6.1 calls a measure, F@A (§2.2); and family, family set, family variant, and the V/M/B anchors keep the shipped meanings they carry throughout. The theory grants that retention in the same section that makes the change — downstream specifications and implementations may retain v5 vocabulary during migration, while the v6 terms define current theoretical usage — and the reciprocal holds:
Retained implementation vocabulary does not redefine the Theory of Data.
Theory evolution does not automatically enlarge Frame-QL.
Read a word in this manual as an implementation term; read the same word in the Theory as a theory term. Where the two coincide the manual says which one it means, at the point of use — that naming is the seam, and it is not a synonym.
A new ToD distinction becomes Frame-QL syntax or capability only when it is separately implemented, tested, versioned, and documented here. Until then it does not exist in the language (§2.9). This manual takes no position on whether the retained implementation vocabulary is eventually renamed, bridged by aliases, or deliberately kept.
Chapter 1. Basic Structure
1.1 What a query is
A Frame-QL query produces a single output frame. A frame is an assembly of co-anchored value columns at a single anchor; the anchor’s dimensions appear automatically as the leading columns of the frame, and the value columns are exactly the series the query computes. This is the framework’s frame model from the conceptual manual, expressed at the language level: the query says what Manifold to address, what series to compute, at what anchor the result lives, with what filters and ordering. The result is a frame.
1.2 The skeleton
The complete skeleton of a Frame-QL query — the envelope (ADR-035), given here in its fixed clause order:
[EXPLAIN]
[FROM <manifold>]
[WITH <name> = <expression> [, ...]]
SELECT <series_1> [AS <alias_1>], <series_2> [AS <alias_2>], ...
AT { <output_anchor> }
[WHERE <per-series predicate> [AND ...]]
[HAVING <output-frame predicate> [AND ...]]
[ORDER BY <output-frame column> [ASC|DESC] [, ...]]
[LIMIT n [PER { <anchor coordinates> }]]
SELECT and AT are required; every other clause is optional. FROM is optional and defaults to the bound Manifold — the surface the session or tool is attached to — and is required only when a surface holds more than one Manifold and the statement must name which (Chapter 1.3). ON is not a query clause — a population is pinned in definitions, and universe is resolved structurally at query time (Chapter 1.5, the §2c law); an ON in a query is a syntax error. Clause order is fixed: the clauses are optional but may not be reordered, and a misordered clause is reported as a syntax error naming the expected order. A statement may prefix the whole query with EXPLAIN (Chapter 1.7), which validates and plans without executing. Each clause has a precise role developed in the following chapters; the important thing here is the overall shape — a query declares a frame, one structural aspect per clause.
The query produces a frame whose anchor is the output anchor named in AT, whose value columns are the series listed in SELECT, and whose rows are determined by the data, the filters, the ordering, and the limit.
1.3 The FROM clause
A query may name its Manifold explicitly (this is the FROM clause shown alone, not a whole query):
FROM finance_manifold
FROM is optional, and defaults to the bound Manifold. The demo, the MCP query tool, and the agent are each attached to exactly one Manifold, and against a single-Manifold surface a bare SELECT … AT {…} resolves against that bound Manifold — no ceremony on the common case. FROM earns its keep on a multi-Manifold surface: where a surface holds more than one Manifold, a statement that names none is not underdetermined-by-guess, it refuses, naming the available Manifolds and asking which. The clause exists so that any surface — a query console, an agent, an MCP client, a planner shared across several Manifolds — can address different Manifolds by naming the target in each statement, without carrying Manifold identity as ambient session state. A statement that names its Manifold is self-contained: it means the same thing regardless of who submits it, from where, with what session history — and is therefore reproducible, cacheable, and auditable on its face; and an unnamed one is reproducible all the same, because the resolved identity always rides in the annotation (below).
Resolved identity is never silent. A Manifold is a versioned artifact, and the result’s annotation always records the resolved identity — (Manifold, version, refresh, certificate state) — so that even a statement that named neither the Manifold nor a version is exactly reproducible after the fact, from its own annotation. An EXPLAIN shows the resolved identity before anything runs. (▸ Frame-QL revision, shipped-law — [SCHEDULED]: explicit version pinning — a VERSION n on FROM —
is designed but not yet in the shipped envelope grammar; it enters by ruling (ADR-035 D1), not by
default. [SCHEDULED] (not [ROADMAP]) because the surface is designed and committed, only unshipped —
the resolved version is always disclosed today, so nothing is lost by pinning arriving later.)
Surfaces may supply FROM on the user’s behalf — licensed by disclosure. A user or client working against a single Manifold should not have to type its name in every statement, and the formal language does not bend to spare them: the resolution is the surface’s job. A surface whose context makes the target Manifold unambiguous injects the FROM clause before submission (the sanctioned sugar of Chapter 8.3). What licenses the injection is the disclosure machinery: the annotation’s canonical form carries the statement as executed, FROM and resolved version included, on every round trip — so the convenience can never become hidden state. The user need not know the clause exists; the record always shows it.
Injection is convenience, never authorization. A surface that exposes one Manifold makes others unaddressable through that surface — which can feel like access control, and is not. A programmatic client against the same planner may write any FROM it likes. Entitlement is enforced at the governance layer — the Manifold author’s access rules (the reserved ACCESS construct; an unentitled statement is the refuse outcome of Chapter 7.3) — and the surface’s injection sits strictly on the ergonomics side of that line.
Cross-Manifold analysis happens through composition, not through cross-Manifold queries: two Manifolds are composed into a third (by inheritance or joining, per the framework manual’s Chapter 8), and the composed Manifold is then queried like any other. There is no inline cross-Manifold join construct in the language — and a shared planner does not change this: it routes each statement to exactly one Manifold; the multi-Manifold question is answered by composing first and querying the composite.
1.4 The SELECT clause and the output anchor
The SELECT clause lists the series expressions that will become the value columns of the output frame, and ends with AT <output_anchor> declaring the output anchor of the whole query:
SELECT sum(revenue @ {transaction}),
sum(cost @ {transaction})
AT {customer}
Several structural points are worth being explicit about, because they govern how the language reads:
Series expressions are output columns. Each series in the list becomes one value column of the output frame. There is no other way for a series to enter the result — the SELECT list literally names the value columns of the frame.
The output anchor is a single declaration for the whole query. All series in a query share the output anchor, by structural necessity: the result is one frame, a frame’s columns are co-anchored, so all the value columns in the result are at the one declared anchor. The AT clause is therefore a query-level declaration, not a per-series one. There is no syntax for series at different output anchors in one query — different anchors require different queries.
The output anchor’s dimensions appear automatically as leading columns. A query at AT {customer, region} produces a frame whose first columns are customer and region — the anchor coordinates that identify the rows — followed by the value columns computed by the series expressions. The writer does not list anchor dimensions in SELECT; they are implied by AT and produced automatically.
The output anchor is required. A query without an AT clause is ill-formed and refused. The framework does not infer an output anchor from context, does not default to {}, and does not guess. (A specific tool or surface may choose to apply an implicit default at its own discretion — typically by interpreting “no anchor” as AT {} for ad-hoc exploration — but this is tool-level sugar, not part of the language. The formal language requires the anchor to be named.)
1.5 AT and @: output anchors versus input anchors
Every anchor specification in the language is one construct with one meaning, written with one of two keywords depending on position. The construct is anchor ascription:
expr @ ameans the value ofexpr, materialized at anchora.
The writer states where the value is wanted; the framework supplies whichever anchor-action realizes the ascription — aggregation through the relevant family when the expression’s grain is finer than a, broadcast (replication) when it is coarser, identity when they are equal. The legitimacy of the realization is checked by the ordinary machinery (the family’s B-anchor, the verified hierarchy edges, the order requirements); the ascription itself never names the mechanism, only the destination.
The destination, stated in full, is a pair. An anchor names a coordinate signature; it does not by itself determine which points inhabit it — two columns addressed by the same coordinates can live on different declared universes (the expected hourly readings and the alerts that happened to fire are both at {sensor, hour}, and are not about the same points). Every ascription is therefore implicitly expr @ (a, U): the anchor names the coordinates, the universe defines the points. The universe is resolved STRUCTURALLY, never pinned at query time (the §2c universe law): every column expression evaluates inside exactly one universe, and a single expression that spans two populations is a cross_universe category error, not a hedged answer. Which universe a column belongs to is fixed in its definition — ON is a definition-language clause, not query syntax — so a query never re-declares a population; it is disclosed: the resolved (anchor, universe) of every series rides in the result’s annotation. A frame that genuinely needs several populations juxtaposes them — separate columns, each evaluating in its own universe, standing together at a shared anchor (Chapter 2.4).
The choice of universe is consequential because a universe declares its basis, and the basis fixes which points the frame expects — the completeness question, and the corrected B3 law (columna#148). In an events population the points are only where something occurred, so an empty bucket is no occurrence, not a hole in a grid; in a spine or product population the full grid is expected, so a missing point is a genuine gap the figure discloses (an incomplete_data caveat, never a silent value); in a registry, membership is a checkable fact (present vs absent). (Frame-QL’s shipped universe types are event and spine. Product and registry are spine sub-bases — refinements of the shipped spine type — and spine, product, and registry all resolve to spine.) What an absent point’s value denotes, however, is not read off the basis: a universe says which points exist; it never says what an absence at one of them means (that basis-keyed default was the D4 defect, removed in #148). The meaning of an absence is the measure’s fill rule Φ — zero (the quantity existed and was nil), unknown (a value existed, unrecorded), or undefined (outside the population) — declared once on the measure and the same at every anchor (the M-contract; columna#143). An events-derived SUM/COUNT typically declares Φ=zero and a state-valued level Φ=unknown, but that follows from what kind of quantity the measure is, not from the universe type. Serving follows the declared basis for the grid and the measure’s Φ for absence; the resolved basis, the fill rule, and any gap ride in the annotation. (The full universe model — bases, declarations, co-universality — is the framework manual’s, and Φ is the Manifold’s; this manual specifies only what the query language exposes: structural universe resolution, the one-expression-one-universe law, juxtaposition, and the disclosure of the resolved (anchor, universe), basis, and fill rule.)
This single rule covers every appearance of an anchor in the language. revenue @ {transaction} ascribes a column to its grain. sum(revenue @ {transaction}) @ {customer, month} ascribes a reduction’s result to an intermediate grain. revenue @ {} in a share-of-total ascribes a column to the Manifold scalar, realized by aggregation, then consumed at a finer grain, realized by broadcast. These are not three constructs; they are one construct in three places.
The two keywords mark the one structural distinction that remains:
@ writes an ascription in input position — the expression’s value at the named grain feeds a larger expression.
AT writes the one ascription that is nobody’s input — the query’s final output anchor. It appears exactly once per query, at the end of the SELECT list, and applies to all series.
The two are not interchangeable, and the framework refuses queries that use @ where AT belongs or vice versa — not because they mean different things (both are ascriptions) but because the query’s final output is structurally unique, and the language keeps that uniqueness lexically visible.
Two anchors, one measure — the compatibility note. This manual invokes the Two Anchors law (§1.6, after the paper The Two Anchors of a Measure it is named for): a reduction’s input anchor is half its denotation, so avg(revenue @ {day}) and avg(revenue @ {month}) are different columns with different keys. That law is about the source and target structure of a reduction or pinned expression — the a_in and a_out of the canonical form op(input @ {a_in}) @ {a_out} (§2.1). It is not the claim that one measure carries two current anchors. Under ToD v6.1 a measure has one current anchor; a reduction is two-anchor because it consumes a measure at one anchor and produces one at another, and family identity may preserve an earlier anchor where that anchor was constitutive of the family. Neither reading moves anything in the language: @ marks an input anchor wherever it appears, AT declares the one output anchor, and the parser is unchanged.
1.6 Series names and the AS alias
Each series in a query’s SELECT list becomes a value column of the output frame, and the column needs a name. A series expression may declare its name explicitly with AS:
SELECT revenue AS total_revenue,
(revenue - cost) AS profit
AT {customer}
When AS is not provided, the framework names the column by its canonical expression — verbatim, not a mechanical rewrite. A column’s identity is what it is: either the name you give it with AS, or the expression itself. No name is invented, and none is mangled.
Bare column reference. The column’s own name. revenue → revenue.
Member access. The dotted reference, verbatim. revenue.sum → revenue.sum (not revenue_sum — the framework does not rewrite the dot; the key is the expression).
Single reducer applied to a column. The reduction, verbatim, input anchor and all. avg(revenue @ {day}) → avg(revenue @ {day}); max(revenue @ {customer*month}) → max(revenue @ {customer*month}). The input anchor is part of the column’s identity (per the Two Anchors law, §1.5), so it appears in the key — two reductions that differ only in their pin are different columns with different keys, never a collision on an invented name.
Any other expression. No derivable identity. The writer must supply AS. This includes:
- Arithmetic and other map expressions:
revenue - cost,revenue / orders. - Conditional expressions:
if(...),case ... end. - Bracket-filtered column references:
revenue[region = "east"]. - Composite reductions:
max(sum(revenue @ {transaction}) @ {customer, month}).
For these the canonical expression is not a single-atom identity the framework will key on, so the query is ill-formed without AS.
Name collisions are refused, not silently resolved. If two series in the same query resolve to the same key — the same canonical expression with no AS on either — the query is refused with a structured error naming the collision. The framework does not append disambiguating suffixes or otherwise guess; the writer must declare an alias for at least one of the colliding series. The same rule applies if a series’s key (canonical or aliased) collides with an anchor dimension’s name (anchor dimensions appear automatically as leading columns of the output frame and own their names).
Keying on names across versions. Because an unaliased column’s key is its canonical expression, it will change if you rewrite the expression — and the mechanical defaults of earlier versions (sum_revenue, revenue_sum) are gone (contract_version "2", 0.14.0). A consumer that needs a stable handle should key on an AS alias: an alias is author-owned and will never change under any future rule. Symmetrically, since a column’s key already is its expression, EXPLAIN and the canonical form print no redundant X AS X: a series whose key equals its expression prints bare, and only a distinct, author-owned alias prints an AS clause.
Alias visibility. An alias declared in SELECT is visible in:
HAVING, which references series by name to filter post-reduction.ORDER BY, which references series (or anchor dimensions) by name to sort.LIMIT n PER {dims}, where thePERdimensions must be anchor coordinates (and thus appear by their dimension names, not aliases).
An alias is not visible in:
WHERE, which is evaluated before reduction; the series do not exist yet, and the alias has no meaning.- Other series expressions in the same
SELECT. Each series is computed independently from the inputs; a series cannot reference another series’s alias. Writers wanting reuse must repeat the expression or use a tool-level sugar that expands to repeated expressions.
Valid alias names. An alias is an identifier subject to the same rules as a column name: alphanumeric with underscores, not starting with a digit, not a reserved keyword. The alias must not collide with anchor dimension names, with another series’s alias or default name, or with reserved identifiers in the Manifold’s namespace.
The aliasing rules require explicitness exactly where the canonical expression is not a single-atom identity (composite expressions, bracket filters, name collisions) and key by the expression itself everywhere else (bare columns, member access, single reductions). The writer is asked to name a series only when the framework cannot identify it by its own expression.
(One mechanism deliberately complements the alias rules: the macro binding of Chapter 4.5, WITH name = expression, which provides the expression reuse that aliases do not — an alias names an output column after reduction; a macro names an expression template before everything.)
1.7 Statements: queries and EXPLAIN
A statement is a query, optionally prefixed with EXPLAIN:
EXPLAIN
FROM finance_manifold
SELECT max( sum(revenue @ {transaction}) @ {customer, month} ) AS peak_month
AT {customer}
EXPLAIN performs everything short of execution — parse, macro expansion, desugar to canonical form, type-check, plan, certificate consultation — and returns, instead of a result frame:
- the canonical desugared form of the query, every sugar expanded, every inferred anchor and reducer made explicit;
- the planner’s atom decomposition: the single-output metric atoms of the form
op(input @ a_in) @ a_outthe plan comprises, and the planner-level map expressions evaluated over their co-anchored results (Chapter 2.1); - the dependency cone — the integrity preconditions the query relies on, each with its current verdict;
- the complete would-be annotation: every disclosure the executed query would carry, and any clarification it would raise.
EXPLAIN never touches data. It is the validation surface of the propose-validate-refine loop: for an agent, the cheap inner loop that turns the substrate’s checking into a feedback signal before any execution cost; for a human, the audit of exactly how the framework read the query. A statement that EXPLAINs clean executes identically when resubmitted without the prefix — same canonical form, same plan — modulo certificate changes between the two calls.
Chapter 2. Series Expressions: The Canonical Form
This chapter specifies the series — one output column’s expression — in its fully-explicit canonical form: the form the planner type-checks against, where every reduction carries its input anchor @ {a_in} and the statement carries its output anchor AT {A}. The sugars of Chapter 3 let the writer omit an input anchor when it is determinable, and desugar to this form; the canonical form is the bedrock. Under the envelope (ADR-035, Chapter 1), @ {…} is the input-anchor marker universally and AT {…} is the sole output-grain declaration — one meaning for each marker, everywhere. (In this manual, series expression — and series used as its shorthand — names a SELECT item, the shipped syntactic object, and nothing else. The word is also used in the framework’s own vocabulary for the ungoverned shape a column has before citizenship; that object has no spelling in the query language, and is not what series means here.)
2.1 The shape of the canonical form
Canonically, a series is a reduction (or a map, or a composite of both) over column references, each reference carrying its input anchor, and the whole statement carrying one output anchor:
SELECT op( col_1 @ {a_1}, col_2 @ {a_2}, ... ) AS name
AT { A }
Every grain is named: every reduction reads an explicit input grain and produces the output grain A; a nested subexpression carries its own input anchor. The framework parses this form directly, type-checks it, and plans it. The sugars of Chapter 3 are normalizations to it.
Two facts give the canonical form its standing, and they are two views of one thing.
Semantic: under the anchor-ascription rule (Chapter 1.5), every sub-expression of a canonical-form statement has a local, self-contained meaning — this value, at this anchor — so the meaning of the whole is the composition of the meanings of the parts, with no ambient context to consult.
Architectural: the canonical form is the planner’s decomposition target. The planner receives a statement, expands and desugars it, and decomposes the plan into atoms — single-output metric requests of exactly the canonical shape, op(input @ {a_in}) @ {a_out}, each carried to the metric engine as a typed retrieval specification (the operator name, the input, the input and output anchors, any order specification, the resolved missing-behavior) — plus the map expressions the planner evaluates itself over the atoms’ co-anchored results. The metric engine answers each atom from its cache or by issuing the few specific backend calls; the engine never sees the statement, only atoms, and is envelope-blind — the planner owns assembly and the envelope’s clauses. A canonical-form statement is therefore not merely a fully-explicit notation: it is its own execution decomposition, readable off the text. This is why the canonical form is the bedrock — it is simultaneously the type-checked meaning of the statement and the unit of planning — and why EXPLAIN (Chapter 1.7) can return the atom decomposition as a faithful account of what execution will do.
2.2 Bare names: inputs versus complete series
A name like revenue may be one of two things, distinguished by where it appears:
As an input to an outer expression — for example, revenue inside sum(revenue) AT {A} — a bare name names the column’s family. It refers to the column under its default family’s reducer, sourced from the column’s root. The outer expression consumes the family; the framework brings the column from its root to wherever the outer expression needs it, applying the default-family reduction as part of getting there. The input position is well-defined for bare names because the outer expression’s output anchor and the column’s root together determine what is being consumed.
As a complete series with a matching grain — a bare measure standing alone in SELECT is a series iff its grain matches the output anchor AT {A} (or reaches it by a verified edge, carrying an implicit identity reduction). SELECT revenue AT {region} is well-posed when revenue reaches region. A bare measure that would require a reduction to reach AT but names none does not serve: it clarifies input_anchor_ambiguous, which must say at what grain, and how, it collapses. A family variant — the measure under one named reduction of its family — is addressed with a dot — level.last, level.sum — the same suffix addressing the anchor grammar uses (Chapter 1); SELECT level.last AT {store} reads the end-of-period inventory snapshot per store. A family variant is not the anchored object this implementation calls a member — retained implementation vocabulary for measure @ anchor, which ToD v6.1 calls a measure, F@A (Preface, The Theory of Data). The variant names which reduction of the family is meant; the anchored object names the family at one anchor. Both appear in this manual and they are different things.
2.3 Single-series reducers and the input pin
A reducer applied to a column, with an explicit input pin and the statement’s output anchor:
SELECT sum(revenue @ {transaction}) AS gross AT {customer}
SELECT max(revenue @ {transaction}) AS peak_txn AT {customer}
SELECT avg(aov @ {day}) AS typical AT {customer}
The first sums transaction-grain revenue up to customer — the natural extensive aggregation. The second takes each customer’s maximum transaction. The third averages the customer’s daily average-order-value.
The third’s choice of input anchor matters, and this is the central rule the canonical form enforces: when the answer depends on the input grain, the input anchor must be explicit. “Average per customer” is not well-defined without saying what is averaged — daily values? weekly? per-transaction? — and each gives a different number. For fertile reducers on a column with a unique natural grain, the input grain is determined and the sugar of Chapter 3 supplies it; for mules (avg, mean, a rate, a snapshot) and for genuinely underdetermined cases, the pin must be named. When it is omitted and the grain is underdetermined —
SELECT avg(aov) AT {customer}
— the framework does not guess. It clarifies on the shipped reason input_anchor_ambiguous, naming the candidate input grains as substitutable alternatives. The same reason covers the multi-input case — a reduction over several inputs whose common input grain is not determined — raised once per contested dimension (OF-1: one reason per contested dimension). An average with no declared input grain is a question, not an answer.
▸ Second-Edition sync (tombstone). The First Edition named the multi-input case with a distinct reason,
co_anchor_ambiguous. That reason is retired — tombstoned 2026-07-16 under the §2c expression law, with a retirement-pin test asserting it is never emitted. The cross-universe rate it originally named is now thecross_universecategory error (§2.5), and within one universe the denotation rule leaves nothing to disambiguate, so the shipped planner emitsinput_anchor_ambiguoushere. The spelling is kept only as a dated tombstone, so old transcripts stay interpretable.
The input anchor may pin a product of levels — the composite input anchor. A pin is not restricted to a single level: avg(revenue @ {store*product*cal.month}) pins the inner reduction at the (store, product, month) product grain, then averages those figures to the output anchor. This is the genuine two-stage statistic — a mean of per-(store, product, month) revenues — and the product pin is exactly what makes it a determinate question rather than an underdetermined “average revenue.” Two laws adjudicate the pin against the output grain:
- Law 1 — the pin may not be coarser than the output (shipped reason
pin_coarser_than_output, REFUSE). When an output level functionally reaches a pin level — the output is finer than the pin along that axis — the coarser pin cannot resolve at the finer output without inventing rows it does not distinguish; the framework refuses, with a pin-specific teaching message and two lawful edits. - Law 2 — no redundant pin (shipped reason
redundant_pin, CLARIFY). When two pin levels are cross-comparable — one functionally determines the other — the pair fixes one axis, not two; the framework clarifies, offering the two admissible pins, never a silent pick.
The single-level pin is the n=1 case of the same rule and is byte-identical to before. (The composite pin arrived in 0.13.4 — WP-GRAIN-1; the engine’s reduce_series_to_anchor was already product-grain-native, so the release lifted the grammar’s single-level restriction and adjudicated the pin × output-anchor lattice. It minted the two reasons above inside the existing vocabulary and changed no wire contract.)
2.4 Map expressions
A map combines co-anchored columns point by point:
SELECT (revenue @ {customer, day}) - (cost @ {customer, day}) AS profit AT {customer, day}
SELECT (revenue @ {transaction}) / (orders @ {transaction}) AS aov AT {transaction}
The operands of a map must be co-anchored — the same input anchor for all. Operands natively at different grains must first be brought to a common grain (by reduction, or by broadcast of a coarse value down a functional edge — §2.6). A map preserves the anchor: co-anchored inputs give a co-anchored result. A map may also appear inside a reducer — sum( (revenue @ {transaction}) - (cost @ {transaction}) ) AT {customer} computes per-transaction profit, then sums to customer grain.
A common anchor is, in full, a common (anchor, universe): columns combined at a shared anchor are checked for co-universality. Combining measures from different universes in one expression is not a hedged answer — it is the cross_universe category error (§2.5), on the query-error channel, not a map failure. To place several populations in one view, juxtapose them (separate series, each in its own universe — §2.5), never combine them in one expression.
2.5 One expression, one universe
Every column expression evaluates inside exactly one universe and never crosses the boundary — the §2c expression law (Chapter 1.5). A single expression that combines measures from more than one population is a category error, returned on the query-error channel as cross_universe (not a mood), naming the two legal paths: ask each population separately, or reconcile them into one. The retired cross-universe “wedge” — a single rate whose numerator and denominator live in different populations — lands here.
A frame in one universe needs no qualification at all: the population is implicit and the frame resolves (single-universe sugar). When several populations genuinely belong in one view, the frame juxtaposes them — separate series, each evaluating in its own universe, standing together at a shared anchor:
SELECT revenue AS revenue,
level.last AS inv
AT {region}
Here revenue evaluates in its population and inv in its own; the frame carries both, each honest about where it comes from. ON is not part of the query grammar — a population is pinned in definitions, never re-declared at query time; universe is resolved structurally from each series’ measures. The resolved (anchor, universe) of every series rides in the annotation (Chapter 7.1).
2.6 Broadcast
When a column is brought to a finer anchor than the grain it arrives at, the anchor-action is a broadcast (the prior edition’s fillup):
SELECT (revenue @ {customer}) / (revenue @ {}) AS share_of_total AT {customer}
The denominator revenue @ {} is the Manifold-wide scalar — {} being the defining boundaries collapsed to one point — broadcast unchanged to every customer for the division. Broadcast has a single semantics — replicate: every finer point receives the same coarse value, whether intensive or extensive. Its purpose is to make a coarse value available at a finer grain for a downstream comparison (share-of-total, deviation-from-group-mean) — never to distribute a total into a finer-grained quantity. Proportional down-allocation is not an operation in the language.
The double-count hazard of replicating an extensive value — spreading a total and summing it back up — is foreclosed structurally: a broadcast value carries, in the B-anchor of any reducer that would sum it, the dimension family it was broadcast along, so an attempt to re-aggregate it back across that family is caught by the ordinary B-anchor check (Chapter 5.3) and refused. (▸ shipped-law reconciliation, 2026-08-20, ADR-036: this read “served, if at all, only with the corresponding critical disclosure” until the crossing became a structural refusal.) There is no allocation rule to supply and none to get wrong.
2.7 Composite reductions
Two reducers compose when one applies to the result of another, with an explicit intermediate anchor:
SELECT max( sum(revenue @ {transaction}) @ {customer, month} ) AS peak_month AT {customer}
For each customer, the maximum monthly revenue. The inner sum(revenue @ {transaction}) @ {customer, month} produces monthly revenues per customer; the outer max reduces those monthlies to one peak. The intermediate anchor @ {customer, month} is essential — “max revenue per customer” is ambiguous without saying what kind of revenue (daily? monthly? transactional?), and omitting it clarifies input_anchor_ambiguous. A composite reduction is not a separate construct; it is the anchor-ascription rule applied recursively — nested ascriptions, each local, read by the planner as two atoms, the inner feeding the outer (§2.1). Any composite reduction requires an AS alias, since no defensible default name can be derived from a nested expression (Chapter 1.6).
2.8 Subsetting and scans [ROADMAP]
Two further expression forms are documented as the language the envelope grows into by ruling (ADR-035 D1); they are not part of the shipped envelope (columna-core 0.18.1), and the ratified grammar spec keeps them roadmap (they are grammar the engine does not yet have). They enter the language when ruled in, not by accretion.
The bracket filter restricts a column reference to a subset of its domain — revenue[region = "east"] — producing a degenerate column that carries its restriction as part of its identity (framework manual, Chapter 6). It differs from WHERE (Chapter 4.1): WHERE restricts the whole statement’s input; a bracket filter restricts an individual reference within one expression. Whether an emptied bucket is even expected — a point on the grid — is decided by the destination universe’s basis (Chapter 1.5): an events population has no point where nothing occurred, while a spine/product population expects the full grid and reports a missing one as a gap carrying incomplete_data. What an emptied-but-expected bucket denotes — a zero, or an absent value — is the measure’s fill rule Φ, not the basis (columna#148); the resolved basis, the fill rule, and any gap ride in the annotation.
Scans are order-dependent, anchor-preserving operations — cumsum(revenue @ {customer, day}), lag(...), rolling_mean(..., window=7d) — whose output at each point depends on an order and on neighbors. The order is derived from an orderable axis in the anchor (day) or named explicitly; an order-dependent operation whose order is neither derivable nor named is a clarification. The family-aware parameters reset / within / step lean on the verified dimension-family structure to express calendar and hierarchy intelligence directly (year-to-date via reset = year; same-period-last-year via step = year); each names a coarser level in the order axis’s family, reached by a verified climb, and a parameter riding a CONTRADICTED edge carries that finding. (The full scan reference is Appendix A.)
2.9 The grammar grows by ruling
The envelope is deliberately small, and it grows by ruling (ADR-035 D1), not by accretion: a construct enters the language when it is ruled in, and until then it does not exist. This is why Chapter 6’s examples carry a shipping mark — an executable example runs against the shipped package or it is marked pending/roadmap; the manual is structurally incapable of documenting a query it did not run. What ships through columna-core 0.18.1 is the envelope of Chapter 1 with the series forms of §§2.1–2.7: reductions with explicit input pins — single-level or a product grain (§2.3, the composite input anchor of WP-GRAIN-1) — maps, composite reductions, the anchor product, one universe per expression, every grain either determined or pinned — and every answer one of the four moods (serve · disclose · clarify · refuse). The same rule governs the theory: theory evolution does not automatically enlarge Frame-QL. A Theory-of-Data distinction enters this language when it is implemented, tested, versioned, and ruled in — not when it is published.
Chapter 3. Sugars
The canonical form of Chapter 2 is precise but verbose. Two sugars permit common cases to be written more compactly, with the framework expanding them mechanically to the canonical form before type-checking. Each sugar has a precise applicability condition; outside that condition, the canonical form is required.
3.1 Sugar: default-family reduction implicit
When a column reference appears in a position that requires a reduction to land at the outer expression’s anchor, and the column’s default family’s reducer is the intended reduction, the reducer may be omitted. The column reference col @ a (or just col as an input naming the family) is read as defaultReducer(col) applied appropriately.
In practice this means a writer can usually write revenue instead of sum(revenue) when revenue’s default family is sum:
revenue AT {customer} -- sugared
sum(revenue @ {transaction}) AT {customer} -- canonical (assuming revenue's root is {transaction})
The sugar applies in two combined ways. First, the default reducer is inferred from the column’s declared default family — the framework looks up the column-spec, finds the default family, and uses its reducer. Second, when the column reference has no explicit input anchor and the path from the column’s root to the output anchor is determined (a single fertile path), the input anchor is taken to be the column’s root.
Both inference steps are mechanical and deterministic. The framework refuses to apply the sugar when either inference fails: when the column has no default family (so there is no default reducer), or when the path from root to output anchor is ambiguous (so the input anchor cannot be inferred).
Some specific cases where the sugar does not apply, and the canonical form is required:
-
Mule reductions.
mean,median,weighted_mean, and similar mules are mules precisely because their answer depends on the input grain. The default sugar cannot apply to a mule (the input grain must be explicit), so the canonical form with explicit@ aand the reducer name is required. -
Non-default-family reductions. When the writer wants
maxorminor any reducer other than the column’s default, the reducer must be named. The sugar covers only the default. -
Ambiguous paths. When a column’s root can reach the output anchor by multiple paths (because the dimension lattice has independent hierarchies the path goes through), the framework cannot infer which path is meant. The writer must specify the input anchor explicitly via the canonical form.
3.2 Sugar: omitting @ root(col) for direct fertile reduction
A specific common case deserves separate notice because it is the most-used form. When a column reference appears as an input to a fertile reducer, and the column’s root is the deepest grain involved in the reduction, the input anchor @ <root> may be omitted:
sum(revenue) AT {customer} -- sugared
sum(revenue @ {transaction}) AT {customer} -- canonical, when revenue's root is {transaction}
This is technically a corollary of the path-determinism case of Sugar 3.1, but it is so common that it is worth stating directly. When the writer omits @ a and the framework infers it as the column’s root, that inference is silent and automatic, and the resulting form is what almost all simple queries look like.
3.3 What sugar does not do
Two things the sugar mechanism explicitly does not do, because both would invite ambiguity the framework refuses to resolve silently:
No implicit output anchor. The output anchor (AT A) is always required and never inferred. A query without AT is ill-formed and refused, regardless of what could be guessed. This is the strict rule from Chapter 1.4.
No implicit reducer for non-default families. A bare column name is read as its default family; if the writer wants a non-default reducer, the reducer must be named. The framework does not infer “you probably meant max” from context.
The combination is: the language gives the writer one defensible default (the column’s declared default family with the column’s root as the input anchor, when the path is determined), and otherwise requires explicit specification. The agent and any sugar layer may apply additional defaults as tool-level conveniences, but the formal language does not.
Chapter 4. Clauses
4.1 The WHERE clause: pre-query input filtering
WHERE restricts the input to the query — applied before reductions, narrowing what data the reducers see. Its predicate references dimensions and column values at the input grain (the data being reduced), not the output grain (the result of the reduction).
FROM finance_manifold
SELECT sum(revenue) AT {customer}
WHERE region = "east" AND date >= "2024-01-01"
This computes per-customer revenue over transactions where region is east and date is in 2024 or later. The framework restricts the input data to the predicate’s satisfying rows, then performs the reduction over the restricted input. The output is per-customer sums of east-region 2024+ revenue.
WHERE predicates may reference any dimension or column at the input grain of any series in the query. They are evaluated before reduction, so they see the raw data; references to series expressions (which are output columns) are not permitted in WHERE — those belong in HAVING.
WHERE is a query-time expression-level restriction, not a Manifold-level state change. The Manifold’s columns retain their declared coverage; the query’s result is restricted to what the predicate permits.
▸ Second-Edition sync (literal quoting at
WHERE, shipped behavior). AWHEREpredicate is currently evaluated by the backend, so its string literals follow the backend’s literal dialect: on the shipped DuckDB connector a string literal is single-quoted (WHERE day >= '2024-06-01'), and a double-quoted token is read as an identifier, not a string — soday >= "2024-06-01"fails to bind. Normalizing FrameQL’s own literal quoting at theWHEREboundary is a rowed follow-up; until then, single-quote string literals inWHERE.
When series have different input grains, the predicate applies per series, at each series’ own input grain. A query whose series draw from {transaction} and from {warehouse, day} under WHERE region = "east" restricts each series’ input independently: each predicate dimension must be reachable from that series’ input anchor (directly as a coordinate, or through a verified hierarchy edge — region reached from transaction via transaction → customer → region). If a predicate dimension is not reachable from some series’ input anchor, the query is underdetermined for that series — there is no defensible way to apply the restriction — and the framework asks for clarification rather than silently applying the predicate to some series and not others.
4.2 The HAVING clause: post-query output filtering
HAVING filters the output frame after the series have been computed. Its predicate references the output columns — the series expressions and the anchor dimensions — and is applied after reduction:
FROM finance_manifold
SELECT sum(revenue) AS total_revenue,
count(*) AS transaction_count
AT {customer}
HAVING total_revenue > 10000
This computes per-customer total revenue and transaction count, then keeps only the rows where total_revenue exceeds 10,000. The framework applies the reduction first, then evaluates the HAVING predicate against each row of the output frame.
HAVING references output columns by name — a series’s alias or default name, or an anchor dimension’s name. It cannot reference the input grain (that information has been reduced away by the time HAVING evaluates). The prior edition also permitted referencing a series by repeating its expression; that form is removed — deciding when two expressions are “the same” (before or after desugaring? with or without inferred anchors?) raised more questions than the convenience answered — and the reuse it served is covered cleanly by macro bindings (Chapter 4.5), which expand identically at every site by construction.
The distinction between WHERE and HAVING is precise: WHERE filters input rows (before reduction, referencing input-grain columns); HAVING filters output rows (after reduction, referencing output-grain columns). Some predicates make sense as both — “region = east” can be a WHERE on input rows or a HAVING on an output that includes region — but the meaning differs: WHERE restricts what is reduced; HAVING restricts what is shown.
4.3 The ORDER BY clause
ORDER BY specifies the sort order of the output frame:
ORDER BY total_revenue DESC, customer ASC
Multiple sort columns are evaluated in left-to-right priority. The framework permits sort by any output column — anchor dimensions or series expressions (referenced by alias or expression). ASC is the default direction; DESC reverses.
ORDER BY is also consulted by LIMIT n PER (next section) to determine within-group ordering when partitioned truncation is requested. The same ORDER BY clause serves both purposes: it is the canonical ordering of the result.
4.4 The LIMIT n clause and LIMIT n PER {dims}
LIMIT n keeps the first n rows of the ordered result:
ORDER BY total_revenue DESC
LIMIT 100
This keeps the 100 customers with the highest total revenue — a flat top-N truncation.
LIMIT n PER {dims} keeps the top n rows within each group defined by the named dimensions:
ORDER BY region, total_revenue DESC
LIMIT 5 PER {region}
This keeps the top 5 customers (by total_revenue) within each region — partitioned top-N. The result has 5 rows per region (or fewer if a region has fewer than 5 customers): 5 from the east, 5 from the west, and so on.
The clause has precise requirements:
The PER dimensions must be coordinates of the output anchor. Partitioning by something not in the output frame is meaningless — there is nothing in the result to partition by. The framework refuses PER {region} if region is not part of the query’s output anchor.
The PER dimensions must be a subset of the ORDER BY columns. This makes the within-group order deterministic: the columns in PER group the result, and the remaining ORDER BY columns rank within each group. Without PER ⊆ ORDER BY, the within-group ranking would be unspecified.
Ties are broken by the remaining ORDER BY columns. If 5 customers tie for the 5th-highest revenue within a region, the framework consults the next ORDER BY column to disambiguate. If ORDER BY does not fully disambiguate within PER groups, the framework discloses the under-determination at plan time and asks for clarification at execution time only if the ambiguity actually arises and matters for the truncation.
The empty PER set is permitted and reduces to flat LIMIT n. LIMIT 5 PER {} partitions the result into one group (the whole result) and takes the top 5 — equivalent to plain LIMIT 5. The form is uniform; the empty case is the degenerate flat case.
The LIMIT n PER form is genuinely common in analytics — top products per category, top customers per region, top events per session — and is awkward in conventional SQL. Having it as a first-class clause makes the language match the natural shape of the questions analysts ask.
4.5 The WITH clause: named bindings
The WITH clause declares named bindings used by the statement. There are two kinds, sharing a clause but doing different jobs.
Allocation bindings [ROADMAP — no allocation operator ships] — WITH allocation <name> = <rule> — declare the partition-of-unity weighting an aggregate-across a many-to-many relationship uses (Chapter 5.6, Example 6.12). They are semantic declarations consumed by the planner. (The undeclared aggregate-across is a clarification, per Chapter 5.6; allocation is one of the readings it names, and the planner says so in those words — “WITH allocation — supply a partition-of-unity split [ROADMAP — not available in this build]” — rather than silently splitting.)
Macro bindings — WITH <name> = <expression> — are a purely syntactic device: name an expression once, reference it by name. Expansion is textual and happens before desugaring and type-checking — each reference is replaced by the bound expression, and then every ordinary rule applies at the site of use. Everything about macros follows from “textual, pre-everything”:
- A macro may be referenced in
SELECTseries,WHERE,HAVING,ORDER BY, and laterWITHbindings. Whether the expanded expression is valid at a site is decided by that clause’s ordinary rules — a post-reduction expression expanded intoWHEREfailsWHERE’s rules exactly as if it had been written there by hand. The macro mechanism grants no new powers; it removes repetition. - Later bindings may reference earlier ones. Recursion, direct or mutual, is refused.
- A series that consists of exactly a bare macro reference takes the macro’s identifier as its default name — the writer has literally named it — so
WITH profit = revenue - cost … SELECT profit AT {customer}needs noAS. Any larger expression involving a macro follows the ordinary naming rules of Chapter 1.6. - Macro names share the namespace of columns, dimensions, and aliases; collisions are refused.
- Macros carry no semantics of their own. The canonical form of a statement is the canonical form of its full expansion, and
EXPLAINshows the expansion — there is no macro in an atom decomposition.
A worked example:
FROM finance_manifold
WITH profit = (revenue - cost)
SELECT profit AT {customer, month}
desugars (assuming co-anchorable defaults) to SELECT (revenue - cost) AS profit AT {customer, month} — and the same profit binding may simultaneously appear in HAVING profit > 0, expanding identically there.
In the shipped envelope’s fixed clause order, WITH precedes SELECT (it binds the names the series then reference); its lexical position carries no scoping meaning beyond that ordering — bindings are visible throughout the statement. (The distinction from AS is worth restating because SQL offers nothing like it: an alias names an output column after reduction and is visible only where output columns exist; a macro names an expression template before everything and is visible wherever expressions are.)
Chapter 5. Type Rules
This chapter specifies the rules the framework applies when type-checking and planning a query. The rules divide into two kinds, and the division matters (Chapter 7 develops it fully). Well-formedness rules are about whether the query is an executable instruction at all — whether it determines one answer. A query failing a well-formedness rule cannot be executed, and the framework responds with a structured clarification naming what is underdetermined. Soundness checks are about the analytical risk of an executable query — coverage gaps, missingness mechanisms, contradicted preconditions. (▸ shipped-law reconciliation, 2026-08-20, ADR-036: B-anchor crossings left this list. A crossing is not a risk attached to a lawful answer; it is an operation the governed law does not grant, and it is refused — §5.3.) Per the inform-and-serve doctrine, a query that fails a soundness check is not withheld on that account: it is executed, and the risk is returned as a disclosure bound to the result, with severity, quantification, and remedy. The framework withholds a result only when it cannot execute (well-formedness), when the data is not there, or when a declared governance rule forbids it — never on its own analytical judgment.
5.1 Output anchor required
Every query must specify an output anchor via AT. Queries without AT are ill-formed and refused. This is the strict rule from Chapter 1.4.
5.2 Anchor compatibility
The output anchor of every series in a query must be the same (the query’s output anchor). Since series expressions are checked individually and the language requires shared output anchor by structural necessity (Chapter 1.4), this rule is enforced by the AT being a query-level declaration rather than per-series.
For inputs within an expression, anchors must compose correctly:
-
For maps: all operands must be co-anchored. The framework checks that all input column references resolve to the same input anchor. If they do not, the writer must bring them to a common anchor explicitly (by aggregation or broadcast) before mapping. A common anchor is, in full, a common (anchor, universe): columns combined in one expression at a shared anchor are checked for co-universality — same declared universe, verified by reference. A single expression combining measures from different universes is not served with an asymmetry disclosure; it is the
cross_universecategory error (§2c, Chapters 2.5, 7.3), on the query-error channel. Several populations in one view coexist by juxtaposition — separate series, each evaluating in its own universe at a shared anchor — never by combination in one expression. -
For aggregations: the input anchor must be finer than or equal to the output anchor in the relevant dimensions. The framework checks that the input-to-output path is valid (via drops and climbs, per the framework manual’s Chapter 4).
-
For broadcasts: the input anchor must be coarser than the output anchor. Broadcast is replicate-only; no allocation is involved. The result carries the broadcast family in the B-anchor of any summing reducer, so it cannot be silently re-aggregated back along the axis it was spread across (Chapter 2.6).
5.3 B-anchor checking
For every reduction in a query, the framework checks whether the reducer’s B-anchor for the column includes any of the dimension families being eliminated by the reduction:
- A
drop(removing a coordinate from the anchor) crosses the B-anchor when the column’s family-set entry for the reducer lists the dropped axis’s dimension family. - A
climb(replacing a coordinate with a coarser one in the same family) crosses the B-anchor when the family-set entry lists the climbed family.
A B-anchor crossing is a structural prohibition, not a soundness finding and not a well-formedness failure: the arithmetic is computable, but the governed law does not grant the operation, so the query determines no lawful answer and the framework refuses it — blocked_reduction, no values on the wire. The refusal names the column, the reducer, and the crossed lineage, and offers the two lawful edits: an alternative reducer from the column’s family set applicable along that lineage (typically last for the time axis of a stock), or an output anchor the reduction does not have to cross. The route-around that dissolves other findings does not reach this one: recomputing from a fertile source at the column’s root instead of re-aggregating a cached image changes where the reduction is performed, not whether the law permits it — the recomputed sum crosses the same lineage and refuses the same way. (That is the ruling’s law-preservation point, seen from the engine’s side.) A Manifold author may additionally declare a hard stop (WITHHOLD) on a (column, reducer); that withholding is the author’s governance rule, reported as such, and it is a different thing from this structural refusal, which the engine owes no author a declaration to enforce.
▸ Frame-QL revision (generated-family law, 2026-08-20). Ratified as ADR-036, superseding ADR-020’s inform-and-serve rule for this case only: structurally prohibited analytical operations refuse. Disclose exists inside the lawful region; it cannot legalize an operation the governed law does not possess. Until this ruling the paragraph above read served, bound to a critical disclosure — the number was produced and the prohibition rode out with it. The mechanism of that error is now on the record: a caveat attached to a number that should never have been produced is what let the same meaningless total keep being served, one wrapper at a time. The check is on the operation, not on the leaf — every point in an expression where a reduction actually travels, whether the reader wrote the reducer or generated it, is adjudicated against the governed ancestry’s B-anchor over the lineages it crosses — so
level.sum,sum(level.last @ {day}),-level.sum,2 * level.sum, a scan over any of them, and aDERIVEDcolumn carrying one all refuse alike. Family generation creates a new analytical family. It does not create a new operator permission. The scope of applicability is untouched: per operator × lineage, no stock/flow/rating type (ADR-031 D5 clarified, not reversed) —sumof a stock across stores, andavg/min/max/countof a stock across time, all remain lawful, because the author barredsumalongcalendarand barred nothing else. The disclosure codeb_anchor_crossingis tombstoned as a producer, retained and still wired (7.4).
▸ Frame-QL revision (locus). The B-anchor crossing is detected statically by the planner — the B-anchor and the path’s eliminated families are knowable from the spec, before any data is touched — and it is the planner that attaches the critical disclosure. What is not static is whether a route-around dissolves the crossing; that is a resolution fact, so the column engine may, during execution, recompute from a fertile root and downgrade the finding to an informational route-around (
RECOMPUTED_FROM_DETAIL), or leave itcriticalwhen no sound route exists. The disclosure thus originates at the planner and has its final severity settled at the engine; in every case the number is served. This is the inform-and-serve boundary in miniature: the planner names the risk, the engine never withholds on it. (▸ 2026-08-20, ADR-036: the locus finding stands verbatim — detection is static, from the spec, at the planner. Its verdict does not, and neither does the downgrade path it describes: a crossing is refused at the planner outright, and no engine route-around dissolves it, because recomputing from detail performs the same prohibited travel. Read this note for where the check lives; read the note above it for what the check now decides.)
5.4 Family selection
When a column reference appears with a non-default reducer named, the reducer must be in the column’s family set. Calling a reducer on a column whose family set does not declare it is refused; the framework lists the available reducers in the family set.
When the default-family sugar applies (Chapter 3.1), the framework uses the column’s declared default family’s reducer.
5.5 Order specification for order-dependent operations
Every order-dependent operation (scan, ordered reducer) must have its order determinable:
- The order may be derived from the input anchor when the anchor contains an axis with a natural order (typically a temporal axis).
- The order may be specified explicitly in the operation’s syntax.
If neither applies — the operation is order-dependent but no order is determined — the framework refuses, naming the operation and the missing order specification.
For the family-aware scan parameters (reset, within, step — Chapter 2.8): the named level must be a member of the same dimension family as the scan’s order axis, reachable from the order’s grain by declared roll-up edges. A level outside the order’s family, or unreachable from its grain, leaves the parameter underdetermined — a clarification. The climb the parameter rides is checked like any climb: its edge’s integrity verdict enters the query’s dependency cone, and a CONTRADICTED edge surfaces as a disclosure on the served result.
5.6 Many-to-many
Aggregating a measure across a many-to-many relationship has a precondition the relationship does not supply — functionality, the partition property that makes per-bucket totals reconcile to the grand total. The check applies specifically to aggregate-across operations; queries that use the many-to-many in other ways (filtering, listing memberships, querying either side independently) invoke no such precondition and are unaffected.
When a query aggregates across a many-to-many without a declared resolution, the framework serves the membership aggregation — each fine entity contributing its full value to every bucket it belongs to — bound to a disclosure stating that the precondition is unmet: per-bucket totals overlap, their sum exceeds the grand total by the overlap amount (quantified in the finding), and the result answers a membership question (“revenue of products in each category”), not a partition question. The disclosure names the three resolutions as remedies: a membership filter (accept the overlap deliberately), a primary designation (convert the relationship to functional), or an allocation declared via WITH allocation (Example 6.12), which supplies the partition-of-unity that makes totals reconcile.
A query that declares one of the three resolutions is computed under it with no precondition finding.
▸ Frame-QL revision (column-foundation). The paragraph above describes a join-based engine, where the membership aggregation is always computable and so can be served with an overlap disclosure. Frame-QL’s engine (ADR-031) transports a measure along functional edges and never joins; a non-functional (M:N) edge is not a traversable path, so the membership aggregation is not expressible at all without one of the three resolutions. The undeclared aggregate-across is therefore caught by the planner as a well-formedness clarification — “this aggregate-across rides a non-functional edge and is underdetermined; supply a membership filter, a primary designation, or
WITH allocation” — returned statically, before execution, with no result. The three remedies are unchanged; only the outcome class moves, from serve-with-disclosure to clarify, because the engine cannot fabricate a number it has no path to. A declared resolution makes the query well-formed exactly as before. (This is the one place the inform-and-serve doctrine yields to well-formedness: the engine still never withholds a number it can produce — here it genuinely cannot.)
▸ Frame-QL revision (RELATE faces, v0.11). The clarification above is now a menu. A relationship may declare faces — named crossing dispositions — and the undeclared-aggregate-across clarify lists the declared faces with their folklore as its alternatives, so the human re-issues by naming one. A face names the value’s disposition on the trip, never the selection criterion; the three are a self-teaching verb triad, mutually exclusive and jointly covering the sensible crossings of a many-to-many by an additive quantity:
touch— the value reaches every match. Per-bucket totals overlap and their sum exceeds the grand total (and, where the fine side has members in no bucket, can also fall short of it — both skews disclosed). This is the membership question (“revenue of products in each category”), now executed, not just described.assign— the value goes to exactly one match, a declared canonical pick. DeclaredFACE a = ASSIGN BY <driver> ORDER MIN|MAX(the driver is a declared measure;ORDERis mandatory, no default — “top” is ambiguous across driver kinds: rank-like →MIN, score-like →MAX). The engine restricts the bridge to each member’s top-ranked pair and single-counts, so per-bucket totals reconcile to the grand total; the dropped non-top memberships are disclosed as the shadow (wirememberships_unrepresented, material → disclose). Collapses the M:N into a functional edge.alloc— the value splits across matches by a declared weight. DeclaredFACE a = ALLOC BY <driver>(noORDER— it splits by the per-member-normalized driver, it does not rank); per-bucket totals reconcile to the cent, certified by the reconciliation badge{crossed_total, base_total, delta, tolerance, status}— an immaterial commutation certificate when it reconciles, material on a shortfall. TheWITH allocationconstruct of §6.12 is this face’s per-query cousin.A face is declared on the relationship (not parameterized per query —
EXPLAINstays static and the cache keys the name), described per the folklore rule, and adjudicated at publish: a face is closed by default, and its license opens the crossing. Faces are addressed as a qualified coordinate,{<coordinate>.<face>}:RELATE product <-> category VIA product_categories(product_id, category_id) FACES { touch = TOUCH -- "revenue reaches every category a product sits in — multi-counted; totals exceed the grand total" assign = ASSIGN BY category_rank ORDER MIN -- "revenue lands in the product's single primary category; the rest disclosed as the shadow" alloc = ALLOC BY category_weight -- "revenue splits across a product's categories by weight; totals reconcile to the grand total" } NOTE "a product belongs to up to 3 categories"SELECT revenue AT {category.touch} -- disclose: over_count (material) + coverage (info) SELECT revenue AT {category.assign} -- disclose: memberships_unrepresented (the shadow of dropped memberships) SELECT revenue AT {category.alloc} -- serve: reconciliation certificate (immaterial unless it shortfalls)The bare
{category}stays uniformly barred and now clarifies with the face menu; each faced coordinate executes and honours its disposition.{category.touch}executes the join-multiply and serves in disclose, carrying the over-count as a material caveat and the coverage (the covered/uncovered count) as an informational one;{category.assign}single-counts and discloses the shadow;{category.alloc}reconciles and serves the badge. Naming honesty holds:category.touch≠category.assign≠category.alloc≠category, each marked in the grain name. Per-basis licensing, v1 = events only — the crossing serves on an events population, where the arithmetic is honest; on a spine/grid it refuses (events-only in v1), because replication or redistribution would corrupt the grid’s own completeness claim, until that thinking lands.Crossed-grain absence follows the measure’s Φ, not the crossing (columna#149). Fill is applied at the member, before the transport, so a face carries whatever fill produced and performs no fill of its own. Absence a face creates — a target category that no measure-side member reaches — belongs to the target member and follows that measure’s fill rule Φ (Chapter 1.5):
zerofills 0, and an undeclared rule discloses (undeclared_absence) rather than filling a silent zero.▸ Second-Edition sync (the triad completed, 0.12.0). The First Edition documented
touchonly —assignandallocwere declared-but-deferred, a fail-closed parse refusal. That deferral is retired: 0.12.0 shipped the full triad, and all three faces now execute (wire additive;contract_versionstayed"1"—relates[].faces[]gaineddriver,assignanswers carrymemberships_unrepresented,allocanswers carryreconciliation {crossed_total, base_total, delta, tolerance, status}). Adjudication is fail-closed, per scheme, at publish:assignrequires a unique top per member (a tie names the tied members and the affected products);allocrequires a non-negative driver with a strictly-positive per-member sum (a zero-sum member is an undefined split), and the driver must be a spine at the frontier grain (an events-derived driver is frozen first). The anchor law (G5) holds across all three: a distinct-class measure — a count-distinct or sketch reducer — refuses at every face, because its output anchor is spent at the frontier grain (per-member counts cannot be summed, weighted, or routed); the message speaks the declaration dialect (distinct(...)), never the sketch representation. The chain guard (G4) refuses a multi-hop face path — one frontier at a time (the composite-pin × faced-output corner below is one instance).
▸ Second-Edition sync (the composite-pin × faced-output corner, OF-26). The composite input anchor (§2.3) and a faced output coordinate meet in one corner the shipped engine does not yet serve: a composite pin whose product includes a faced output coordinate —
sum(revenue @ {product*category.touch}) AT {category.touch}— resolves the inner at a base+faced composite grain, which trips the G4 chain guard and refuseschained_crossing(one frontier at a time). The plain faced outputrevenue AT {category.touch}serves unchanged; the corner is a named refusal, never a silent number, rowed out (WP-GRAIN-1 residue) until a real case demands the disclosure-stacking across a face at a base+faced grain that it needs.
5.7 Coverage compatibility
When a query combines columns at a shared anchor, the framework consults the coverage states of the columns:
- Complete columns combine freely. No coverage finding is produced.
- Filtered columns combine over the intersection of their restrictions. The result inherits the intersection as its coverage; the result’s lineage records the intersection, and the annotation states the universe the answer reflects. If the restrictions are disjoint, no data supports the combination — the framework returns no result and refuses: this is a data-gap outcome, not an analytical judgment.
- Filtered combined with complete produces filtered. The complete column is implicitly restricted to the filtered column’s domain for the combination; the result inherits the filtered column’s restriction, disclosed in the annotation.
- Sampling restrictions combine differently from predicate restrictions, and the framework says so. Two columns each filtered by an independent random sample intersect, in expectation, on the product of their fractions — two independent 1% samples share ~0.01% of the universe — which is rarely the analysis anyone intends. The combination is served over the realized intersection with a critical disclosure quantifying the surviving fraction, unless the samples are declared coordinated (drawn over the same units), in which case they combine like any shared restriction.
- Quarantined columns do not participate in cross-column combination. A quarantined column is one whose incompleteness the author has not declared usable — its exclusion is the Manifold author’s standing coverage governance, and the framework reports it as such: the query returns no result for the affected combination, refusing it because the column is quarantined and naming the remediation (declare the coverage with its restriction, or address the defect at the source). This withholding belongs to the author’s governance, not to the engine’s analytical judgment.
Coverage in the marginal-mismatch range (between the Manifold’s configured thresholds) is served with the coverage state disclosed; combinations across a coverage gap carry the gap, its asymmetry, and the resulting bias direction in the annotation. The coverage machinery is part of the framework’s universe-integrity layer (per the framework manual’s Chapter 10), and its findings carry the certificate state of the preconditions they derive from.
Chapter 6. Examples
This chapter shows worked examples that exercise the language across its common shapes. Each example shows the query, the canonical desugared form where it differs, and a brief note on what the query computes.
6.1 Simple aggregation
FROM finance_manifold
SELECT revenue AT {customer}
Canonical: FROM finance_manifold SELECT sum(revenue @ {transaction}) AT {customer} (assuming sum is revenue’s default family and {transaction} is revenue’s root).
Computes per-customer revenue total.
6.2 Multiple metrics at a shared anchor
FROM finance_manifold
SELECT revenue,
cost,
count(*) AS transaction_count
AT {customer, month}
Computes per-customer-month revenue, cost, and transaction count. All three series share the output anchor {customer, month}.
6.3 Composite reduction with explicit intermediate anchor
FROM finance_manifold
SELECT max( sum(revenue @ {transaction}) @ {customer, month} ) AS max_monthly_revenue
AT {customer}
Computes, for each customer, the maximum monthly revenue. The intermediate anchor @ {customer, month} is required — it specifies that the question is about monthly maxima, not daily or transaction-level maxima. Without the inner anchor, the question would be ambiguous and the framework would ask for the grain (a clarification, per Chapter 7). The AS alias is also required for any composite reduction, since the framework cannot derive a default name from a nested expression.
6.4 Mean with explicit input anchor
FROM product_manifold
SELECT mean( engagement_score @ {customer, week} ) AT {customer}
Computes the per-customer mean of weekly engagement scores. The mule reducer mean requires the input anchor to be explicit — “average engagement per customer” is meaningless without saying what kind of engagement is being averaged.
6.5 Map of co-anchored columns
FROM finance_manifold
SELECT (revenue - cost) AS profit AT {customer, day}
Computes per-customer-day profit by subtracting cost from revenue at the daily grain. Revenue and cost must be co-anchorable at {customer, day}; the map combines them point by point.
6.6 Ratio across grains
FROM finance_manifold
SELECT ( revenue @ {customer} ) / ( revenue @ {} ) AS share_of_total
AT {customer}
Computes each customer’s share of total revenue. The numerator is per-customer revenue; the denominator is the Manifold-wide grand total ({} being the Manifold’s defining boundaries collapsed to one point). The denominator is broadcast — replicated unchanged — to every customer for the division.
6.7 Bracket filter on a column
FROM finance_manifold
SELECT revenue[region = "east"] AS east_revenue,
revenue AS total_revenue
AT {customer}
Computes per-customer east-region revenue alongside total revenue. The bracket filter restricts the revenue column to its east-region subset; the result column appears alongside unrestricted revenue.
6.8 WHERE: pre-query filtering
FROM finance_manifold
SELECT revenue AT {customer}
WHERE date >= "2024-01-01" AND region IN ("east", "west")
Restricts the input to transactions in 2024+ and east or west regions before computing per-customer revenue. The output is per-customer revenue over the restricted input.
6.9 HAVING: post-query filtering
FROM finance_manifold
SELECT revenue AS total_revenue AT {customer}
HAVING total_revenue > 50000
Computes per-customer revenue then filters out customers with revenue at or below 50,000.
6.10 Top N per group
FROM product_manifold
SELECT product_revenue AT {category, product}
ORDER BY category, product_revenue DESC
LIMIT 5 PER {category}
Computes per-category-product revenue, sorts by category then by revenue descending, then keeps the top 5 products per category. The result has up to 5 rows per category, with each category’s rows ordered by revenue.
6.11 Scan for running total
FROM finance_manifold
SELECT cumsum( revenue @ {customer, day} ) AS revenue_to_date
AT {customer, day}
Computes the running cumulative revenue per customer, ordered by day. The scan partitions by customer and accumulates by day; the result is co-anchored with the input.
6.12 Many-to-many with allocation [ROADMAP]
The WITH allocation <name> = … form is a grow-by-ruling surface (ADR-035 D1): it is not part of the
shipped envelope grammar, so the block below is marked frameql-illformed — the shipped parser rejects
its WITH allocation head, and the manual’s self-check asserts exactly that until the form ships.
FROM product_manifold
WITH allocation product_to_category = proportional_to(category_weight)
SELECT sum( revenue @ {product} ) AT {category}
Computes per-category revenue when products belong to multiple categories. The WITH allocation clause declares the allocation rule the framework uses to apportion each product’s revenue across its categories; without this declaration, the aggregation across the many-to-many relationship would be served with membership semantics and an overlap disclosure (Chapter 5.6 — or, under the column-foundation, raised as a clarification; see the revision note there); the allocation supplies the partition-of-unity that makes per-category totals reconcile, with no precondition finding.
6.13 Time intelligence: year-to-date and year-over-year
FROM finance_manifold
SELECT cumsum( revenue @ {customer, day}, reset = year ) AS revenue_ytd
AT {customer, day}
Computes per-customer year-to-date revenue: the running sum restarts at each year boundary, the year level reached from day through the verified time family.
FROM finance_manifold
SELECT ( revenue - lag(revenue, 1, step = year) ) / lag(revenue, 1, step = year) AS yoy_growth
AT {customer, month}
Computes per-customer year-over-year revenue growth at month grain: lag(…, 1, step = year) is the same month one year earlier, and the growth is an ordinary map over the co-anchored pair.
6.14 Macro bindings for reuse
FROM finance_manifold
WITH profit = (revenue - cost)
SELECT profit AT {customer, month}
HAVING profit > 0
ORDER BY profit DESC
LIMIT 10
The profit macro expands textually at every reference site before type-checking; as a bare-macro series it takes profit as its default name. The statement desugars to one canonical query with (revenue - cost) AS profit selected, filtered, and sorted — visible in full via EXPLAIN.
6.15 The envelope, end to end
The top-3 stores per region by gross, with each region’s typical order value — the ratified acceptance target for the envelope (ADR-035), exercising anchor products, an auto-leading coarser coordinate, a WITH macro, two different input grains juxtaposed in one frame, and PER ⊆ ORDER BY:
FROM retail
WITH line = revenue @ {transaction}
SELECT sum(line) AS gross,
avg(aov @ {day}) AS typical
AT { region * store }
ORDER BY region, gross DESC
LIMIT 3 PER { region }
Reading it clause by clause: AT {region * store} stands at the (region, store) grid — region auto-leads as the coarser coordinate (the edge region ← store carries it), an anchor coordinate rather than a selected series, and so nameable by ORDER BY/PER. WITH line = revenue @ {transaction} binds the transaction-grain line once; sum(line) reduces it to (region, store) — the reduction is edge-carried (transaction → store → region), the grammar admitting it while the kernel’s edge verdict decides serve vs disclose. typical = avg(aov @ {day}) reads aov at day grain and averages to (region, store) — two different input grains (transaction, day) collapsing into one frame, legal because each series names its own @. ORDER BY region, gross DESC references the frame’s own columns; region leads so groups are contiguous, gross DESC ranks within. LIMIT 3 PER {region} keeps the top 3 per region — well-posed because region is both an anchor coordinate AND an ORDER BY key (the conjoined PER law: PER keys are anchor coordinates only, and PER ⊆ ORDER BY). PER {typical} would refuse (an alias, not a coordinate); PER {store} would refuse unless store is added to ORDER BY.
6.16 Composite input anchor: a two-stage statistic
FROM retail
SELECT avg( revenue @ {store*product*cal.month} ) AS avg_monthly_product_revenue
AT {store}
For each store, the average of its per-(product, month) revenues — a genuine two-stage statistic whose inner grain is a product of levels (the composite input anchor of WP-GRAIN-1, §2.3). The pin @ {store*product*cal.month} fixes what is averaged (monthly per-product figures), and the outer avg reduces those to one number per store. The pin is finer than the output along every extra axis (product, month), so it resolves cleanly. A pin coarser than the output along some axis would refuse pin_coarser_than_output (Law 1); a pin naming two cross-comparable levels — one determining the other — would clarify redundant_pin (Law 2). The avg(revenue @ {store*product*cal.month}) series keys on its canonical expression and needs no AS; the alias here is an author-owned stable handle (Chapter 1.6). Under the earlier column-identity default this same series would have keyed as the pin-eliding avg_revenue — the elision WP-NAME-1 retired (§1.6): the pin is part of the column’s identity and now appears in its key.
Chapter 7. Outcomes, Disclosures, and Clarifications
This chapter specifies what a query returns and what it returns it with. The prior edition specified a refusal taxonomy; this edition replaces it, per the inform-and-serve doctrine: analytical risk is disclosed, never used as grounds to withhold a result the query determines. The chapter specifies the universal return contract, the complete outcome taxonomy, the structure of disclosures, and the control model that governs who may cause a result to be withheld.
7.1 The universal return contract: (result, annotation)
Every query returns a pair:
result— the output frame. It is null in exactly the withholding outcomes of 7.3 (clarify, no-data, governance/authorization), and present in every other case, including every analytically risky case.annotation— always present, on every path. On the clean path it is a single no-riskOKfinding. On served-with-risk paths it is the result’s risk profile. On withholding paths it carries the why and the remedy.
An annotation is a list of targeted findings plus a frame-level severity rollup. Each finding carries: a code (the machine-readable category); a severity (none < info < caution < critical); the originating four-state certificate_state (VERIFIED / CORROBORATED / CONTRADICTED / UNTESTABLE); a targeted subject (the specific metric, column, anchor, or cell it concerns — never smeared frame-wide); a plain-language headline and detail; a quantification where one is computable (a caveat with a number is honest; “may be inaccurate” is noise); the bias direction and bound where determinable; an actionable remedy; a requires_acknowledgment flag; and a provenance_ref linking the finding to the certificate entry or precondition it derives from, so the finding travels bound to the frame and remains auditable.
Beyond its findings, the annotation always carries the canonical desugared form of the statement as executed — every macro expanded, every sugar normalized, every inferred anchor and reducer explicit, the FROM clause and its resolved Manifold identity (Manifold, version, refresh, certificate state) included, and the resolved (anchor, universe) of every series — together with a reference to the plan’s atom decomposition (Chapter 2.1). Every result is thereby auditable against the exact reading the framework gave the query: a human checks the interpretation at a glance, and an agent learns the canonical language from every exchange rather than only from documentation. (EXPLAIN, Chapter 1.7, returns the same material without execution.)
The annotation is computed in full for every query. A Manifold’s disclosure_default (with optional per-call override) controls how much of it is returned — MINIMAL / STANDARD / FULL — as a serialization-time view, never as a control on what is assessed, and subject to a non-negotiable safety floor: the overall severity, every critical finding, and every requires_acknowledgment flag are present at every level. Verbosity may trim noise; it may never silence risk. The canonical form rides at every disclosure level — interpretation is never noise.
7.2 The outcome taxonomy
Four outcomes exhaust the contract — the shipped wire’s four moods, serve · disclose · clarify · refuse (the cross_universe category error rides the separate query-error channel, not a mood):
Serve, clean. Every precondition in the query’s dependency cone is VERIFIED or CORROBORATED, no soundness finding fires. The annotation is the single OK finding.
Serve, with disclosures. The query is executable and determinate, and one or more soundness findings apply. The result is served; the findings ride with it. This outcome has a first-class sub-case, route-around: where the planner can dissolve a risk rather than flag it — recomputing a mule from its fertile sources instead of re-aggregating a cached image; serving from transaction detail when a CONTRADICTED or stale pre-aggregate would otherwise be used — it does so, and discloses the routing as an info finding (RECOMPUTED_FROM_DETAIL, SERVED_FROM_DETAIL) rather than disclosing a risk that no longer exists. Only when no sound route exists does the risk surface as a caution or critical finding on the served number.
Clarify. The query is not an executable instruction: it does not determine one answer. The framework returns no result and opens a structured clarification naming exactly what is underdetermined and what would determine it. How the clarification is conducted is a surface decision — a console may show the message, an agent may ask the question in the requester’s vocabulary, an MCP client may receive the structured payload and resubmit.
Refuse. The query is well-formed but no result can or may be produced for a non-analytical reason: the data is not there, a declared governance rule withholds it, or the governed law does not grant the operation at all — a reduction travelling a lineage its operator is declared BLOCKED along has no lawful reading to serve (§5.3, 7.3). The framework returns no result and states the reason and the remedy. (▸ shipped-law reconciliation, 2026-08-20, ADR-036: the third limb is new, and it is not an analytical-risk withholding. The four moods sort by lawfulness, not by confidence — serve: lawful, no material condition; disclose: lawful, a material condition travels with the answer; clarify: several lawful meanings remain; refuse: no lawful path exists.) (▸ shipped-law: this is the wire’s fourth mood, refuse. The Third-Edition lineage called it inform, under ADR-020’s “inform-and-serve” doctrine — same outcome, same spirit; the shipped name is refuse.)
There is no fifth outcome. In particular there is no analytical-risk withholding: any result that is determinate and producible for an entitled caller is produced, with its risk on its face. (▸ 2026-08-20, ADR-036: “determinate and producible” is to be read as lawful and producible. A structurally prohibited reduction is arithmetically determinate and is still refused — the prohibition is on the operation, not on the confidence of its answer — and no fifth outcome is minted for it: it is an ordinary refuse.)
▸ Frame-QL revision (the doctrine’s locus). The four outcomes map cleanly onto the two-projection architecture (Chapter 2.1). Clarify and the governance/no-data half of refuse are planner outcomes: they are decided statically, from vocabulary and shape, before the column engine resolves anything — ambiguity, a missing anchor, an unknown operator, a type mismatch, a non-traversable edge, a
WITHHOLDor access rule, an empty realized support. (▸ 2026-08-20, ADR-036: the structural refusal joins that list, and the sentence below already reserved its place — “the planner may refuse what is not an executable, permitted instruction.” A blocked reduction is executable and not permitted; the B-anchor is knowable from shape alone, so it is adjudicated there, with zero backend fetches.) Serve, clean and serve, with disclosures are engine outcomes: once a query is well-formed and permitted, the engine resolves it and any surviving analytical risk rides out as a disclosure. The boundary between “may withhold” and “must serve” is therefore the boundary between the planner and the engine — the planner may refuse what is not an executable, permitted instruction; the engine, having an executable one, never withholds on its own analytical judgment. This is why the canonical form is both the type-checked meaning and the unit of planning: the same decomposition that the planner validates is the decomposition the engine serves.
7.3 The withholding outcomes, enumerated
A query yields no result in exactly these cases:
Clarifications — the query cannot be executed as posed:
- Missing output anchor. No
ATclause. What to clarify: the output anchor. - Missing required alias. A series with no defensible default name (composite reduction, map expression, bracket filter, conditional) and no
AS. What to clarify: the name. - Name collision. Two series whose default or declared names collide, or a series colliding with an anchor dimension’s name. What to clarify: a disambiguating alias.
- Anchor incompatibility. Operands of a map that do not resolve to a common input anchor, with no expressed path to one. What to clarify: the explicit aggregation or broadcast that brings them together, or a restructuring.
- Mule without explicit input anchor (shipped reason:
input_anchor_ambiguous, one reason per contested dimension — including the multi-input case). A reduction whose answer depends on an input grain that has not been named. What to clarify: the input pin (Chapter 2.3). (▸ shipped-law reconciliation, 2026-08-20, ADR-036: the menu enumerates the lawful candidate pins only. Where exactly one candidate is lawful nothing is contested, so the framework proceeds on the defaulted anchor and owes the materialinput_anchornote; where none is lawful the ask refusesblocked_reductionrather than clarifying, because an unlawful reading is not a choice the asker can be offered — offering it is how a reader gets talked into a laundered answer one keystroke later.) (▸ shipped-law reconciliation, 2026-07-17: the input anchor is written inline —avg(revenue @ {day})— and a composite pin@ {a*b*c}is admissible, §2.3.) (▸ Second-Edition sync: the First Edition named the multi-input caseco_anchor_ambiguous; that reason is tombstoned (retired 2026-07-16, §2c expression law; never emitted) — the planner emitsinput_anchor_ambiguousper contested dimension, and the cross-universe rate the reason once named is now thecross_universecategory error below.) - Redundant pin (shipped reason:
redundant_pin). A composite input anchor (§2.3) pinning two cross-comparable levels — one functionally determines the other — so the pair fixes one axis, not two (Law 2). What to clarify: which of the two admissible pins. (▸ Second-Edition sync: minted 2026-07-30, WP-GRAIN-1.) - Ambiguous path. A column whose root reaches the output anchor by more than one hierarchy path (sibling hierarchies), unnamed. What to clarify: which family is climbed.
- Attribute keyed at several levels (shipped reason:
ambiguous_grain). An attribute table keyed at more than one level, so the engine cannot infer which level the attribute is a property of — and where a delivery grain is available, it pins none of the candidates uniquely. The engine reports the fact and the candidate frames; it does not choose. What to clarify: the attribute’s level, which the message enumerates (key at '<level>'per candidate). (▸ currency addition, 2026-08-22: shipped code named here for the first time; the condition is long-standing.) - Type mismatch, on the query-error channel (shipped reason:
type_error, on the query-error channel — NOT a clarify, likecross_universebelow). The shipped planner typechecks against the operator’s signature and raisestype_errorwhen an operator or scan is applied to a logical type its signature does not accept, naming the type it received and the types it accepts. The bullet above describes the same condition as a clarification; the shipped classification is the error channel. (▸ currency addition, 2026-08-22: shipped code and channel recorded as emitted. The taxonomic question — whether the manual’s clarify framing or the shipped error channel is the intended reading — is left open here rather than settled in a documentation pass.) - Cross-universe expression (shipped reason:
cross_universe, on the query-error channel — NOT a clarify). A single column expression that combines measures from more than one universe. This is a category error, not a hedged answer: an expression evaluates in exactly one universe (the §2c expression law, Chapter 2.5). What to do: ask each population separately, or reconcile them — and to place several populations in one view, juxtapose them (separate series, each in its own universe). (▸ shipped-law reconciliation, 2026-07-17: net-new — the retired cross-universe “wedge” lands here.) - Order missing. An order-dependent operation (scan, ordered reducer) whose order is neither derivable from the anchor nor specified. What to clarify: the order.
- Family not declared. A reducer named on a column whose family set does not declare it. What to clarify: a declared reducer (the clarification lists the family set), or an extension of the column-spec.
- Unknown operator. An operator name that resolves to no reducer, scan, or map function in the Manifold’s operator registry (distinct from family not declared, which is a known reducer absent from this column’s family set). What to clarify: a registered operator, or an extension of the registry. (▸ Frame-QL revision: made explicit; the planner typechecks operator names against the registry before resolution.)
- Type mismatch. An operator applied to an input whose type its signature does not accept (e.g. a numeric reducer on a String column). What to clarify: a cast, or a type-appropriate operator. (▸ Frame-QL revision: made explicit; signatures are checked at the planner.)
- Non-traversable edge (fan-out) (shipped reason:
non_functional_transport). An aggregate-across that rides a non-functional (M:N) relationship without a declared resolution (Chapter 5.6). What to clarify: a membership filter, a primary designation, orWITH allocation. (▸ Frame-QL revision: under the column-foundation this is a clarification, not a served membership aggregation.) - Unreachable predicate dimension (shipped reason:
filter_unreachable). AWHEREdimension not reachable from some series’ input anchor (Chapter 4.1). (▸ shipped-law reconciliation, 2026-07-17: the shipped envelope planner raisesfilter_unreachablefor aWHEREpredicate whose dimension no series’ input grain can reach.)
Refusals — the governed law does not grant the operation, the query executes against nothing, a pin is incoherent, or a rule withholds it:
- Structurally prohibited reduction (shipped reason:
blocked_reduction). A reduction that travels a lineage its operator is declaredBLOCKEDalong for that column — summing a stock across the calendar (level.sum AT {store*cal.month},level.sum AT {store}), re-summing a broadcast value along the axis it was spread across (§2.6). The arithmetic is computable and the ask is refused anyway: the governed law does not possess the operation, so there is no lawful reading to serve — in any spelling, whether the reducer was written as a declared family member, generated by an inline reducer above a lawful sibling (sum(level.last @ {day})), or carried inside a unary, binary, scalar, scan orDERIVEDwrapper. A lawfulness refusal, shipped as REFUSE/UNSUPPORTED (the same outcome family asout_of_universe), distinct from the coherence, no-data and governance refusals around it. What to fix: the two lawful edits the message names — a reducer that is applicable along the lineage (.lastfor a stock collapsed over time), or an anchor the reduction does not have to cross. (▸ shipped-law reconciliation, 2026-08-20, ADR-036: minted here, and moved here from 7.4, where ADR-020 had placed it as a served critical disclosure. Family generation creates a new analytical family. It does not create a new operator permission. Scope is unchanged — per operator × lineage, no stock/flow type:sumof the same stock across stores, andavg/min/max/countof it across time, all stay lawful.) - Pin coarser than output (shipped reason:
pin_coarser_than_output). A composite input anchor (§2.3) pinning a level coarser than the output grain — an output level functionally reaches the pin — which cannot resolve at the finer output without inventing rows it does not distinguish (Law 1). A coherence refusal — the pin names a grain incoherent with the output — shipped as REFUSE/UNSUPPORTED (the same outcome family asout_of_universe), distinct from the no-data and governance refusals below. What to fix: the two lawful edits the message names. (▸ Second-Edition sync: minted 2026-07-30, WP-GRAIN-1.) - Uncertified transport (shipped reason:
uncertified_edge). A transport the planner did not positively admit a route for. The engine fails closed rather than choosing one: a declaration makes an edge eligible for certification, not executable — so the executed route is always the certified route. Named alternatives: publish/adjudicate so the edge is certified on the attested data, or address at a grain that does not need the transport. (▸ currency addition, 2026-08-22: minted 2026-08-19, after the Second-Edition sync point.) - Uncertified crossing face (shipped reason:
uncertified_face). The same law at the M:N frontier (Chapter 5.6): the ask names a declared face, but adjudication has not positively admitted it — or the admission is stale. A declaration makes a crossing eligible for certification, not executable. Named alternatives: publish/adjudicate so the face is certified, or, where it was refuted on the data, fix the data and re-attest. (▸ currency addition, 2026-08-22: minted 2026-08-20, after the sync point.) - Spent distinct anchor at a face (shipped reason:
anchor_spent). The G5 anchor law (Chapter 5.6) as the engine emits it: a distinct-class measure — a count-distinct or sketch reducer — refuses at every face, because its output anchor is spent at the frontier grain and per-member counts cannot be summed, weighted, or routed. The message speaks the declaration dialect, and names the two lawful readings: declare a weighted composite as a value measure, or ask the crossed-population count. (▸ currency addition, 2026-08-22: minted 2026-08-20; the law was already documented at 5.6, the code was not named here.) - Refuted transport edge (shipped reason:
contradicted_edge). Transport along an edge whose declared functional dependence is refuted on the attested data — a key with more than one parent — so the reduction across it is withheld: serving never outruns the verdicts. Distinct from a contradicted precondition served with a disclosure (7.4): there the planner routes around the contradiction, here there is no route that avoids the refuted edge. Named alternatives: fix the data and re-attest, amend the hierarchy, or address at a grain that does not cross this edge. (▸ currency addition, 2026-08-22: shipped code named here for the first time; the condition is long-standing.) - No data / gap (shipped reason:
incomplete_data/data_gap). The rows are not there: an empty realized anchor, disjoint filtered restrictions, a requested combination the bound data cannot support — or, in a spine/product universe, a gap in the expected grid (per the basis law, Chapter 1.5). The figure covers what is present; the gap is disclosed, never a silent zero. - Quarantined column. A cross-column combination touching a column whose incompleteness the author has not declared usable (Chapter 5.7) — the author’s standing coverage governance.
- Author hard stop. A
(column, reducer)the Manifold author has declaredWITHHOLD(see 7.5): a governance rule, reported as the author’s rule with the declared rationale and the alternative families. - Authorization. The caller is not entitled to the data under the Manifold’s access rules. (The access-rule layer is specified separately; the outcome category is reserved here.)
Everything formerly in this chapter’s refusal taxonomy that does not appear above — mule re-aggregation, many-to-many aggregate-across, coverage mismatch, MNAR, contradicted integrity preconditions — has moved to 7.4: those queries are served, with disclosures. (▸ shipped-law reconciliation, 2026-08-20, ADR-036: B-anchor violations are no longer in that list. The Third-Edition taxonomy refused them; ADR-020 moved them here to 7.4; ADR-036 returns them to the refusals above — this time as a structural prohibition rather than an analytical judgment, which is precisely the distinction the intervening reading lacked.)
7.4 The disclosure catalog
The principal soundness findings, each carrying the fields of 7.1:
-
B-anchor crossing — TOMBSTONED as a producer, 2026-08-20 (ADR-036); retained, and still wired (
critical; shipped code:b_anchor_crossing/blocked_reduction). A reduction coarsening across a blocked family — summing a stock over time (level.sum @ store*cal.month), re-summing a broadcast value along its broadcast axis. This was the canonical served-with-a-critical-disclosure case; it now refuses (7.3), because Disclose exists inside the lawful region and cannot legalize an operation the governed law does not possess. The code is not deleted: archived wires, recorded transcripts and the deposited editions still resolve against it, and the Refuse channel’s reason carries the same spelling — one concept, two channels, so probe the referent and not the spelling. Nothing fresh is emitted under it. What it used to carry as a remedy is now the refusal’s first named alternative (.lastfor a stock over time; Chapter 5.3). -
Mule re-aggregation (route-around,
info;cautionif no route). An attempt to re-aggregate a sterile result is recomputed from the fertile sources at root where they exist (the average is rebuilt from sum and count —RECOMPUTED_FROM_DETAIL); where no fertile decomposition exists (exact distinct count, exact median, mode), the result is recomputed from base at the output grain — the grain at which it is computable — and the finding states it cannot be soundly re-aggregated past that grain, with the sketch-backed approximate family (approx_distinct,approx_quantile) as the reaggregable remedy. -
Many-to-many membership aggregation (
caution/criticalby overlap). Aggregate-across without a declared resolution, served with membership semantics and the overlap quantified (Chapter 5.6); remedies: filter, primary designation,WITH allocation. (▸ Frame-QL revision: this served-disclosure applies to a join-capable engine; under the column-foundation the undeclared case is a planner clarification, not a served result — see the revision note in Chapter 5.6.) -
Contradicted precondition in the dependency cone (route-around where possible; otherwise
caution/critical). A broken hierarchy edge, a failed co-anchoring, a coverage drift: where a route that avoids the contradicted condition exists, the planner takes it and discloses the routing; otherwise the result is served carrying the finding with the contradiction’s specifics (“customer 19847 maps to both ‘east’ and ‘west’”) and itscertificate_state = CONTRADICTED. -
MNAR exclusion (
caution/critical,certificate_state = UNTESTABLE). A reduction over a column whose missingness is declared value-dependent: no correction the observed data supports exists; the result is served as the observed-data figure with the unknown-direction bias stated, never presented as a population estimate. -
Rescaling applied [ROADMAP — no rescaling behaviour ships; a reduction is not silently rescaled] (
info/caution). An extensive reduction under MCAR has been skip-and-rescaled to an unbiased population-total estimate; the finding carries the rescale factor (N_total / N_observed) so the result can be reconciled against a raw source-system sum. -
Coverage (
info→criticalby gap). Restricted universes, marginal overlaps, asymmetric gaps with bias direction, independent-sample intersections (Chapter 5.7). -
Staleness (
info/caution). The result reflects a data version whose refresh timestamps the finding carries. -
Absence under the fill rule Φ (shipped codes below). What an eligible point with no observed value denotes, disclosed per the measure’s declared Φ — the semantics are §1.5’s and are not restated here; the codes carry them onto the wire.
zero-declared absence is filled and served immaterial; every other case is served with the cell left null,unknownand the undeclared case material:code severity Φ what it denotes declared_fillinfo— immaterialzerofilled 0 — the quantity existed and was nil unknown_absencecaution— materialunknownleft null — a value existed but was not recorded out_of_populationinfo— immaterialundefinedleft null — the point is outside the measure’s population undeclared_absencecaution— material(none) left null — no rule declared; the engine discloses rather than choose a value, and never fills
The disclosure severities are findings about the data and the operation; they are never the engine’s grounds for withholding. (▸ 2026-08-20, ADR-036: unchanged, and now unambiguous — a severity never withholds, because everything that reaches a disclosure is already lawful. What withholds is the prior question of §5.3: whether the governed law grants the operation at all.) The safety asymmetry of the certificate is preserved verbatim in the findings: a finding’s certificate_state retains the state a pre-ADR-020 refusal would have minted — CONTRADICTED or UNTESTABLE — demotable at most to CORROBORATED by a passing data check, never silently to VERIFIED.
7.5 The four-party control model
Withholding and handling are governed by four parties, each with a distinct, legitimate authority. The engine’s neutrality among them is the design’s load-bearing property.
The Manifold author exercises corporate governance. The author already controls the Manifold’s entire shape — scope, sources, columns, operators, the V/M/B declarations — and the same authority extends to declared withholding rules: per-(column, reducer) hard stops (WITHHOLD in the definition language), access and entitlement rules, and policy customization of operator behavior. When an author rule fires, the engine reports it as the author’s rule: the refuse outcome names the declaration, its rationale where given, and the permitted alternatives.
The engine withholds only what it cannot do: an ambiguous query is not an executable instruction and must be clarified first; an empty data support cannot be served; and an operation the governed law does not grant has no lawful reading to execute (§5.3). These are the engine’s only inherent non-serves — soft, structural, and free of judgment. (▸ shipped-law reconciliation, 2026-08-20, ADR-036: the third clause is new, and it is structural in exactly the sense the sentence already claims — it reads a declaration the author wrote, not an opinion the engine formed. The author declares BLOCKED { calendar } once; the engine enforces it everywhere, including in the spellings the author never anticipated.) In every other case the engine’s stance is fixed: serve, and disclose. The engine executes governance rules it did not author and produces disclosures it does not act on; it never withholds on its own analytical opinion.
The surface — a query console, the agent, an MCP server, an embedding application — decides how outcomes are handled: how a clarification dialogue is conducted, how a refuse is presented, and what to do with a served result given its disclosure content. Severity-driven presentation gates (blur-until-acknowledged on requires_acknowledgment, confirmation steps on critical findings) are surface decisions, applied to presentation, never API-level withholding — entitled programmatic callers receive the bytes with the annotation, or composition breaks.
The user may declare their own suppression preferences — “do not return results whose disclosures match these conditions” — implemented at the surface, or communicated to the engine as a per-call policy. This is the user governing their own consumption, the symmetric counterpart of the author governing the corpus.
One sentence holds the model together: the author may forbid, the engine may only clarify, report emptiness, or decline what the declared law does not permit, the surface decides presentation, and the user decides their own threshold — and every one of these is visible in the annotation, so no withholding and no risk is ever silent. (▸ 2026-08-20, ADR-036: the engine’s clause gained its third limb. Note what did not change — the engine still forms no analytical opinion of its own; declining a blocked reduction is the author’s BLOCKED declaration being carried out, in every spelling of it.)
7.6 Message conventions
Every clarification, refuse, and finding includes: its category code; the specific column, expression, or precondition concerned; the data version and certificate state at the time; and a remediation where one is determinable. The conventions hold across surfaces because they are properties of the structured payload, not of any one presentation: a clarification like “the inner grain of this mean is underdetermined — name the input anchor (@ {customer, day} or @ {customer, transaction} give different answers)” and a finding like “the roll-up edge customer → region is CONTRADICTED (customer 19847 maps to both ‘east’ and ‘west’); served from transaction detail, routing disclosed” are designed to be acted on — by a person reading them or by an agent in a propose-validate-refine loop.
Chapter 8. Sugar and Extensions
8.1 The role of sugar
Frame-QL is a small language: a query has a fixed clause structure, expressions follow the canonical form, and the sugars of Chapter 3 cover the common-case shortcuts. The agent and various surfaces may apply additional sugar layers — domain-specific shortcuts, natural-language-to-query expansion, embedded query fragments in other languages — that desugar to Frame-QL before reaching the framework.
This is by design. The formal language stays small and analyzable; surface sugars handle ergonomics. The framework type-checks the desugared Frame-QL, so any sugar that produces well-formed Frame-QL is admissible; sugar that produces ill-formed Frame-QL is rejected at the framework boundary, no matter how natural it looked at the surface.
8.2 Name aliases
A Manifold may declare operator aliases through its operator registry — total for sum, unique_visitors for count_distinct(visitor_id), etc. — that are valid in queries against that Manifold:
FROM retail_manifold
SELECT total(revenue), unique_visitors AT {store}
The aliases are Manifold-level (declared in the operator registry), not universal. The same alias may mean different things in different Manifolds, or be undeclared in some; the framework resolves aliases against the named Manifold’s registry.
8.3 Recommended sugar patterns at the surface
Surfaces building on Frame-QL may apply patterns like:
- Implicit
AT {}for ad-hoc exploration queries without explicit output anchor (with the desugared form shown to the user for transparency). - Implicit
FROM <manifold_name>when the active Manifold is unambiguous in the surface’s context — the sugar Chapter 1.3 specifies in full. The formal language requiresFROM; the surface injects it before submission, and the injection is licensed by disclosure: the annotation’s canonical form shows the statement as executed,FROMand resolved version included, on every round trip. Injection is ergonomics, never authorization (Chapter 1.3). - Natural-language-to-canonical-form translation by the agent.
- Domain-vocabulary substitution (the team’s terms mapped to the underlying column and reducer names).
- A pipeline rendering of the canonical form for surfaces whose users prefer left-to-right reading of deep compositions —
revenue | sum @ {customer, month} | max AT {customer}— where each stage desugars mechanically to one nesting level of the canonical form (max( sum(revenue) @ {customer, month} ) AT {customer}). The pipeline is presentation, not semantics: the framework receives, type-checks, and explains only the canonical form.
The framework places no constraints on what surfaces do, as long as what they hand to the framework is well-formed Frame-QL.
Appendix A: Operator Reference
The registry is one umbrella, and it is the planner’s contract with the engine. Reducers, scans, and map functions are three kinds of operator in one installation-level registry. The registry holds, for each operator, its name, its kind, and its type signature; the planner reads exactly this to typecheck an operator against its inputs and to route it — a reducer is decomposed into an atom
op(input @ a_in) @ a_out, a scan is handed to the column engine to run against a derived order, a map is evaluated by the planner over co-anchored results. The mechanics — howsumcombines, how an HLL sketch merges, howrolling_meanwalks a window — live in the engine, never in the registry. This is the same logical/physical split the type system uses: the registry is vocabulary (names, kinds, signatures, fertility, the{mechanism → behavior}map), the engine is implementation. Custom operators and custom datatypes (e.g. anHLLSketch-typed column with anapprox_distinctfertile reducer) are added here, at the registry, which is why a new operator needs no change to the planner’s machinery — only a new registry entry the planner can read. The extension point itself ships: the distinct family is built through it — a custom TYPE (HLLSketch) plus three registered custom operators (hll_count/hll_merge/hll_estimate). The sketch reducers below beyond HLL are [ROADMAP] — the registry can name them; no engine mechanics exist for them yet.
Reducers
| Reducer | Type | Notes |
|---|---|---|
sum | fertile | extensive monoid; carrier is the value |
count, count(*) | fertile | counts present (non-missing) values |
max, min | fertile | idempotent monoids; overlap-robust |
product | fertile | watch for overflow / zero |
any, all | fertile | logical monoids on Boolean |
mean | mule | computed at presentation from sum and count |
weighted_mean | mule | computed from weighted sum and weight sum |
variance, stddev | mule | computed from count, sum, sum-of-squares |
median (exact) | mule | no fertile carrier; recomputed from base at the output grain, not re-aggregable past it |
mode | mule | no fertile carrier; recomputed from base at the output grain, not re-aggregable past it |
approx_distinct | fertile via HLL | HLL sketch is fertile; estimate at presentation. Ships (HLLSketch(p)) |
approx_quantile | fertile via t-digest | t-digest sketch is fertile. [ROADMAP — no t-digest ships] |
approx_frequency | fertile via count-min | count-min sketch is fertile. [ROADMAP — no count-min ships] |
last | ordered, fertile-along-its-order | requires an order; path-invariant along the order |
first | ordered, fertile-along-its-order | requires an order; path-invariant along the order |
value_at_max, value_at_min | ordered, mule | needs the full set |
A note on last and first as family founders: an ordered reducer may appear in a column’s family set (a stock measure’s LAST family is the canonical case — the closing balance over time). Because order is never part of a cache key, what such a family caches is the un-ordered slice of its source; the ordered reduction is applied (or recomputed) at serve time against the declared order. The family is path-invariant along its order — last-of-lasts is the global last — which is exactly the property the serve-time application relies on.
Map functions
Arithmetic: +, -, *, /, %, unary -.
Comparison: =, !=, <, <=, >, >=, between, in.
Logical: and, or, not.
Conditional: if(predicate, then, else), case ... when ... then ... else ... end.
Null/missing: is_null, is_missing, coalesce.
Numeric: log, exp, sqrt, abs, sign, ceil, floor, round.
String: concat, substring, lower, upper, trim, length.
Temporal: year, month, day, week, quarter, date_diff, date_add.
Scan functions
cumsum, cumprod, cummin, cummax — running aggregates (prefix of the fertile reducer).
rolling_sum, rolling_mean, rolling_min, rolling_max, rolling_count — windowed (require a window).
lag(col, n), lead(col, n) — shifted values.
rank, dense_rank, row_number — ordinal positions.
pct_change — relative change from previous.
ewm_mean — exponentially-weighted mean.
Scan parameters, passed by keyword: window (rolling extent), n (shift offset), by (explicit order column), and the family-aware trio reset / within / step (Chapter 2.8), each naming a coarser level in the order axis’s dimension family.
Type predicates and casts
is_<type> — predicate for value type (e.g., is_integer, is_string).
cast(col, <type>) — explicit type conversion.
Appendix B: Reserved Keywords
The shipped envelope statement keywords (columna-core 0.18.1; case-insensitive, whole-word): EXPLAIN, FROM, WITH, SELECT, AS, AT, WHERE, HAVING, ORDER, BY, LIMIT, PER, ASC, DESC, AND. These are the clauses of the envelope (Chapter 1.2), in fixed order; SELECT and AT are required, the rest optional (FROM defaults to the bound Manifold).
Structural markers. @ {…} — the input anchor, on a column reference or reduction, the input-anchor marker universally; AT {…} — the output grain, the sole output-anchor declaration; * — the anchor product (store * cal.month), the same operator as UNIVERSE u = store * day (comma accepted on input, * canonical); {…} — an anchor set (a product of levels). The trailing-@ output form is retired — a top-level @ no longer spells an output anchor (Appendix D).
ON is a DEFINITION-language clause, not a query keyword. A population is pinned in definitions; the §2c universe law resolves universe structurally at query time (Chapters 1.5, 2.5). An ON in a query is a syntax error.
Grow-by-ruling keywords — documented, not yet in the shipped envelope statement grammar (they enter by ruling, ADR-035 D1): VERSION (the FROM … VERSION n pin, Chapter 1.3, [SCHEDULED]); the scan-parameter keywords window, n, by, reset, within, step (recognized in scan-argument position, Chapter 2.8 [ROADMAP]); the bracket-filter [...] syntax on column references (Chapter 2.8 [ROADMAP]); and the expression-level OR, NOT, IF, CASE, WHEN, THEN, ELSE, END, BETWEEN, IN (series-internal expression text is captured verbatim and delegated to the expression parser at plan time, so its dialect grows independently of the envelope). WITH allocation is [ROADMAP] (Chapter 4.5).
Reserved for future use: INHERIT, JOIN, COMPOSE, ASSERT, ACCESS.
Operator names from Appendix A are reserved when used in operator position; the same identifiers may be used as column names in Manifolds that declare them.
▸ shipped-law reconciliation (2026-07-17), RESOLVED. The prior edition of this appendix flagged
FROM/SELECT/AT/{…}as “Coframe canonical form, not the shipped grammar,” pending the Coframe→envelope rewrite. That rewrite has landed: the envelopeSELECT … AT {…}is the shipped grammar (Chapter 1), so these are the shipped query keywords, reconciled above. The retired terse@-fragment (its:label and trailing-@output) moves to Appendix D.
Appendix C: For SQL Speakers — Five Intuitions That Transfer Wrong
Frame-QL deliberately borrows SQL’s surface so that the syntax costs a SQL-literate reader nothing. The price of familiarity is that a handful of SQL intuitions transfer incorrectly, and they are worth naming once, plainly.
1. There is no GROUP BY, because the output anchor is the grouping. AT {customer, month} declares the grain of the result; grouping is implied by the ascription, and the anchor’s dimensions appear automatically as the frame’s leading columns. SELECT lists only the value series — listing a dimension in SELECT is the error, not the habit.
2. WHERE is not one table scan; it applies per series, at each series’ own input grain. In a multi-series query whose series draw from different roots, the predicate restricts each series’ input independently, its dimensions reached through verified hierarchy edges (Chapter 4.1). A predicate dimension unreachable from some series’ input grain is a clarification, not a silent partial filter.
3. There are no joins, and nothing is missing. Combining columns means bringing them to a common anchor — aggregate one side, broadcast the other — and mapping at that anchor. The relationship a SQL writer would join on was declared once, as a verified edge in a dimension family, and compiled into the Manifold; the fan trap is not avoided by care but unrepresentable by construction.
4. SUM may not equal the sum of the stored rows — on purpose, and it says so. Under the defaults (extensive reducer, MCAR missingness), the framework serves skip-and-rescale: an unbiased estimate of the population total, with the rescale factor disclosed in the annotation. Reconciling against a source system’s raw SUM? Pin the family’s missing-policy to skip (or read the factor out of the finding) — the difference is the declared missingness, made visible instead of silently undercounted.
5. Aliases are not variables — reuse is WITH, not SELECT-list chaining. An AS alias names an output column after reduction; it is invisible in WHERE and to sibling series. The reuse SQL writers reach for is the macro binding (WITH name = expression, Chapter 4.5): textual, expanded before everything, valid wherever the expanded expression would be.
Appendix D: Lineage — the retired terse @-fragment
Before the envelope shipped, Columna’s public query surface was a fragment of the language: a single output column, its output anchor spelled by a trailing @.
aov @ cal.month # the fragment: "aov, output-anchored at cal.month"
revenue, orders @ region # a short list, one shared trailing-@ output anchor
name: expr @ anchor # optional `:` label on a column
This form shipped in the pre-launch demo, the What-is-Columna glossary, the site query strings, and early manual drafts. It is retired, and it is not kept as a sugar. The reason is the one collision the envelope exists to kill: the fragment spelled two different things with @ — the outer trailing @ meant the output anchor (aov @ cal.month), while the inner @ of an inline reduction meant the input anchor (avg(aov@day)). Under the envelope’s ruling, @ {…} is the input-anchor marker universally and AT {…} is the sole output-grain declaration, so a top-level aov @ cal.month would now read as “aov input-anchored at cal.month, output anchor unstated” — an underdetermined frame. Keeping the fragment as sugar would mean @ spelled the output anchor in one position and the input anchor in every other; the envelope removes the ambiguity by removing the fragment.
Migration is mechanical. Every terse statement rewrites to the envelope by moving the trailing anchor into AT {…} and any : label into AS:
| Retired fragment | Envelope |
|---|---|
aov @ cal.month | SELECT aov AT {cal.month} |
revenue, orders @ region | SELECT revenue, orders AT {region} |
inv: level.last @ region | SELECT level.last AS inv AT {region} |
avg(aov@day) @ cal.month | SELECT avg(aov @ {day}) AT {cal.month} |
What the envelope absorbed from the fragment: the per-subexpression input anchor (@ {…}), the anchor product *, dotted member addressing (level.last), and juxtaposition of several readings at a shared anchor. What it declined: the trailing-@ output spelling and the : label — both replaced by the unambiguous AT {…} / AS. (This is the Coframe→envelope lineage note of ADR-035 D2: the ideas the shipped grammar kept, and the container ceremony it left behind.)
End of the Frame-QL Manual, Second Edition — current for columna-core 0.18.1 / columna-server 0.11.1 and wire contract "4" (a mechanically-reconciled continuation of the First Edition, whose sync point was columna-core 0.14.0 / 0.13.4 and contract "2"; itself a renamed continuation of the Coframe-QL Manual, Third Edition, reconciled to the shipped envelope grammar, ADR-035).