Skip to content

rfc: the GQ 3 grammar, one break and the slots it freezes - #1202

Open
ragnorc wants to merge 16 commits into
ModernRelay:mainfrom
ragnorc:claude/query-language-evaluation-5a029b
Open

ragnorc wants to merge 16 commits into
ModernRelay:mainfrom
ragnorc:claude/query-language-evaluation-5a029b

Conversation

@ragnorc

@ragnorc ragnorc commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Why

GQ has grown one capability at a time: a logic word reserved when a filter needed it, ranking placed in order because that is where a score was first wanted, search functions added as keywords, the operator vocabulary amended in place, an explain version per feature, a deprecation shim per rename. Each change was right on its own terms and none was asked to fit the next five, so the language now carries decisions justified only by arrival order, and four open RFCs each schedule a break of their own. A whole-language review against SQL, GQL, Cypher, GraphQL, Datalog and the Elasticsearch, LanceDB and Vespa query languages found the semantics sound and the accretion the problem: every expressiveness gap is a position the grammar never reserved.

This RFC opens a revision of the language as a whole. The goal is a foundation that is evolvable (every later capability has a reserved position), maintainable (one grammar, one function registry, one annotation form, one diagnostic contract), expressive (slots for derived relations, optional patterns, block aggregates as values, retrieval as typed columns, post-group filters, pattern-scoped writes) and extensible (registry rows and declarations an agent can author without a compiler release). It does the breaking work first, once, so everything after it is additive.

Summary

Adds docs/rfcs/2026-10-28-gq-3-grammar.md (maintainer track) and its registry row, and commits the evaluation it is drawn from as docs/rfcs/assets/2026-10-28-gq-3-grammar-evaluation.md (17 axes against SQL, GQL, Cypher, GraphQL, Datalog, Elasticsearch, LanceDB and Vespa; literature; the GQ 3 redesign; the RFC review; staging; TypeDB; direction; the one-way rule). The RFC links it from the Motivation and the decision log.

The RFC decides that GQ makes one incompatible change, edition 3, and then stops making incompatible changes. The release that ships it lands every syntactic decision that would otherwise force a later break; forms whose semantics arrive in later RFCs land as slots the parser accepts and the type checker refuses with a stable code naming the owning RFC.

The freeze list: mutation for writes; the triple traversal kept unchanged ($p worksAt $c, source on the left; <e> for either orientation), with every arrow form a parse error whose fix prints the triple; a retrieve { } clause binding typed score columns, with order sorting values and having filtering them; every function a generic call over a registry, with nothing left in the grammar; the reserved set closed at the six logic words; limit $k; generic @name(args) annotations; reserved declaration kinds (view, batch), blocks (optional { }, block aggregates as values), a view binding with arguments, returning, the meta-fields @type_id/@distance/@score, the schema-catalogue type names, namespaced type references a::b::C, and the schema roles Vector(n, distance=…), @analyzed, @valid; an optional gq 3; edition line.

A rewriter, omnigraph queries migrate, turns edition-2 sources into edition 3 by byte-range edits with an IR-equality proof; the release that ships edition 3 rewrites an edition-2 stored source at boot with a warning instead of quarantining its graph, and the release after removes the edition-2 grammar.

Owners of the slots

Derived relations and optional patterns (new), the RFC 0048 rewrite, the analyzed lexical search RFC (#792), a writes RFC (new), polymorphic types (#884), schema namespaces (#1015). No permissions construct is reserved: the permissions model is not accepted, and a file-level declaration keyword can be added later without a break.

Gates (blocked_on)

  • the in-context competence instrument run on the edition-3 spelling through a parser-only build;
  • the GQT corpus rewritten by the tool with zero hand edits, green, with IR equality on every case.

Checks

python3 scripts/check-docs.py (425 files OK) and typos pass.

Update 2026-10-29: TypeDB, direction, one spelling per meaning

  • One way. The maintainer's requirement that there be one way of expressing something is now a stated design rule (Go's "a language needs only one conditional control flow construct"): a respelling with no meaning of its own is absent from the grammar and named by the diagnostics; a form that falls out of a general construct is canonicalised by omnigraph queries fmt (count { } > 0 → exists { }, count { } = 0 → not { }, later $x.@type = "T" → $x is T). The freeze list is checked against the rule in the Design section.

  • Direction. The left arrow $b <-knows- $a is removed as a second spelling of $a -knows-> $b (GQ has no path chains, so it never continues a line); it is a parse error whose fix prints the swap. An arrow written against the schema is T5 with a machine-applicable swapped clause, the deterministic correction the Cypher ecosystem bolts on after generation (LangChain's CypherQueryCorrector; CypherBench counts a reversed relation in about one gpt-4o failure in six) made loud and moved into the compiler. -e- resolves orientation per member from the endpoint types (the issue 659 alternation case), and T22 narrows to the genuinely ambiguous case.

  • TypeDB 3 / TypeQL. Reviewed against the design (pipelines, functions, try, type variables, roles, the 2-to-3 migration, its agent benchmarks). It confirms the slot strategy (TypeQL's grammar carries lists, structs and three annotations its docs call "not yet available") and the rewriter (TypeDB's break had no query-migration tool), and changes no slot: its pipeline closure is the alternative the RFC rejects (text order is plan order, rows cross-product, reads and writes mix) and is now an Alternatives row; its type-variable binding and row-valued given parameters are additive in GQ and noted for the polymorphism and a later RFC; roles are an Alternatives row.

  • Schema queryable at any size. A sixth property in the Motivation: the schema is data in the same language. Catalogue types (@Type, @Property, @Edge, @Interface, @Index, @Function, @View) are node types for every purpose of the grammar, so a thousand-type schema is bound, traversed, searched (retrieve { $t.lex: bm25($t.instruction, $q) }) and counted instead of dumped into a context window. The @-prefixed catalogue edge selector ($t: @Type -@property-> $p: @Property) is reserved so catalogue edges never collide with a user edge named property or from; the engine catalogue RFC's schema extension owns the catalogue's edges and searchable fields.

Update 2026-10-30: no arrow

The arrow spelling is withdrawn. The arrowhead carries no information: when the endpoint types differ the schema fixes the orientation, and when they are the same the position does, in a triple exactly as in an arrow form. Its two justifications did not hold up (a triple reads in source-to-target order; the only in-context measurement GQ has is on the triple, with no direction errors, while CypherBench shows arrows do not prevent reversals), and it was the break's largest rewrite. The triple traversal therefore stays unchanged from edition 2, <e> keeps the per-member orientation rule and the narrowed T22, a triple written against the schema is T5 with the swapped clause as its fix, and -e->, <-e- and -e- are recognised parse errors whose fix prints the triple. Catalogue edges are $t: @Type @property $p: @Property. The rewriter table and the compatibility section lose their traversal rows; the break no longer touches every stored query.

One incompatible change to the GQ grammar, edition 3, after which every
later capability is a registry row, a plan node behind existing syntax or
the filling of a reserved slot. The RFC carries the freeze list (mutation
keyword, arrow traversals, a retrieve clause binding score columns, generic
calls over a function registry, the closed reserved set, limit as a
parameter, generic annotations, and the reserved declaration kinds, blocks,
clauses, meta-fields, catalogue names and namespaced type references), the
rewriter that migrates edition-2 sources with an IR-equality proof, and the
one-release window in which edition 2 is rewritten at boot instead of
quarantined.
@ragnorc ragnorc changed the title docs(rfc): add the GQ 3 grammar RFC rfc: the GQ 3 grammar, one break and the slots it freezes Oct 29, 2026
@ragnorc
ragnorc marked this pull request as ready for review October 29, 2026 19:28

ragnorc commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Recommendation: revise.

This review covers head 6dc6315d3bc73217723a5e916312bd94b4970943. The pull request was ready at aa174ee16e9096ccbcdf895eca4ae6de58978313. One later commit changed only docs/rfcs/2026-10-28-gq-3-grammar.md (+102/−13). I checked that delta against the same compiler facts.

Why

Each GQ feature has reserved a word, added a call, or advanced an explain version. Three open drafts still schedule a later major. #805 schedules (3, 0) and then (4, 0). #792 removes search, match_text, and fuzzy at the next major. #793 removes the positional k of rrf at a later major. One edition that reserves the missing positions costs less, when those positions match the engine that already exists.

What this RFC decides

Edition 3 is the one incompatible break. After it, the grammar accepts more text and keeps every produced shape. The grammar parses each reserved position. The type checker refuses a position whose owner has not landed, with a stable code that names that RFC. omnigraph queries migrate rewrites source by byte range and requires the typed IR to stay equal. Edition 2 compiles for one release. Boot rewrites that source in memory and logs a warning. The following release removes the edition-2 grammar.

The latest commit keeps one directed spelling, $a -knows-> $b, with the source on the left. left_arrow exists so the diagnostic can print the swapped form. $a -knows- $b means the orientation the endpoint types admit, including one direction per alternation or wildcard member. That matches the direction the type checker already stores on each member.

Claims that match the code

Checked at the RFC pin ea895066 and at local main e7d95ae74dc1bf8d42aacef35dc0395c804fe6a4. query.pest, schema.pest, GQ_LANGUAGE_VERSION, and EXPLAIN_VERSION are the same bytes at both commits.

  • GQ_LANGUAGE_VERSION is (2, 3) in crates/omnigraph-compiler/src/lib.rs. EXPLAIN_VERSION is 7 in crates/omnigraph-planner/src/explain.rs.
  • The reserved rule is and, or, not, is, null, and in.
  • The named function productions exist, including rank_expr and nearest_ordering. text_search_clause and match_value are still in query.pest.
  • Query annotations are @description and @instruction. Schema keyword arguments use ident = literal.
  • TypedExpr::score_type returns F32 for nearest and bm25, and F64 for rrf. The comment there says Lance writes F32 scores and distances.
  • IRExpr and IROp derive Eq and Hash. search_node in crates/omnigraph-planner/src/optimizer.rs matches IRExpr::Nearest, Bm25, and Rrf and builds LogicalNode::Nearest, TextSearch, and RankFuse.
  • A stored query that fails to parse quarantines the graph (crates/omnigraph-server/src/settings.rs).
  • Catalogue types are the five names in CATALOGUE_TYPES. Query diagnostic codes allow the prefixes Q, T, and P (crates/omnigraph-compiler/src/query/codes.rs). Schema lint codes use OG-….
  • The quoted pre-stable sentence is in the Summary of docs/rfcs/2026-09-24-shared-expression-model.md. The Compatibility section says the language wins where a better language and an unchanged behavior conflict.
  • python3 scripts/check-docs.py on this head: Documentation OK (435 Markdown files). typos 1.50.1 on the two changed files: exit 0. The title passes scripts/check-pr-title.py.

Blocking

1. The retrieve column records the wrong type, and a score having is a new contract

The freeze table says the nearest column and the bm25 column are F64?, null outside the window or null for a non-match. score_type publishes F32 for both, and F64 only for rrf. A GQT projection of this score is F32. Returned rows are the window, or the match set. A row outside the window is absent. A non-match is absent. Lance 13 distance values are f32.

having { $d.vec < 0.6 } is described as the same Filter the overfetch ladder already applies. The ladder widens candidates so a graph or property filter can still fill limit. A distance predicate after the window can return fewer rows than limit. T32 already refuses a retrieval under an aggregate, so a query that has both is unspecified.

Freeze F32 for the nearest column and the bm25 column, and F64 for rrf. Say that rows outside the window are absent. State how having interacts with limit, or keep a score having as a slot until the ladder is specified to see that predicate. score_type and score_column already own the column type. A later Lance distance-type change stays a language break unless the frozen type remains the type score_type already publishes.

2. IR equality cannot prove the ranking rewrite

The rewriter refuses a write unless the edited text compiles to a typed IR equal to the original. Eq on IRExpr compares Nearest, Bm25, or Rrf with a later property access of the arm name. Those trees differ, so the proof refuses every ranking query. The gate that requires a green corpus and zero hand edits cannot pass.

After both editions lower, compare the logical nodes. Both must be the same Nearest, TextSearch, or RankFuse. Treat the generated arm name as a projection alias. lower.rs and those logical nodes already own this. A second IR would copy that state.

A list contains rewritten as in can stay equal when both lower to the same Binary operator. A string contains() can stay equal the same way. Traversal direction lives on the expand operator, so a rewrite that changes direction fails this proof. That failure is correct. Finding 3 is the rewrite that causes it.

3. The rewriter table disagrees with the direction rule, and it omits live spellings

The traversal section says the fix prints -> with the ends in schema order. The table maps $a knows $b to $a -knows-> $b and leaves the variables in the written order. An incoming pattern ($company worksAt $person when worksAt runs from person to company) changes Direction. The equality proof then fails.

The table also omits $a * $b, $a <*> $b, and $a "in" $b. The wildcard resolver includes an edge in either direction between the two types (EdgeSelector::Wildcard in crates/omnigraph-compiler/src/query/typecheck.rs). The form that keeps both orientations is $a -*- $b. The freeze table lists $a -*-> $b, which keeps one orientation and omits the incoming members. The sentence "Everything not in the table is unchanged text" leaves the omitted spellings unparsed under an arrow-only grammar.

A traversal clause still accepts hop bounds. {1,3} is already in the table. The accurate claim is that one clause is not a path chain, so a left arrow is not required to continue a line.

The compatibility list names the triple traversal. Name every removed spelling: the bare triple, undirected angle brackets, the edge binding, alternation, the wildcard, the quoted edge, and the left arrow (an edition-3 recognizer only).

4. The proposed file rule omits statements the grammar accepts today

query_file accepts show, branch, commit, load, explain, or many query_decls, plus statement_trailer and missing_param_list. The sketch accepts one undefined statement, or declaration*. declaration* also matches an empty file. The summary says branch and commit statements stay. missing_param_list is the diagnostic for a missing (), which is what the competence gate measures (RFC 0047). Extend query_file. Keep missing_param_list.

5. @example and @if_commit are marked working, and annotation_args has no production

The compiler accepts @description("…") and @instruction("…") on a query. @example and @if_commit are absent. --if-commit is a request precondition. Mark those two names as slots, or specify them as new behavior. The later sentence, that the writes RFC must choose one owner for @if_commit and the request field, is the right open question. It conflicts with "the typed four work" in this row.

annotation_args is used and has no production. Schema annotations already use ident = literal (annotation_kwarg in schema.pest). Call options in this RFC use :. Define annotation arguments with =, the production .pg files already use. A grammar that applies : to @embed("text", model="…") refuses text this RFC says a .pg file may still contain.

6. (3, 0) collides with open drafts that still schedule their own majors

The current language version is (2, 3). Explain is 7. #805 (draft, head 49f436ce618b) assigns (3, 0) and (4, 0) to the infix-to-call change. This RFC also assigns (3, 0), to a larger break. #793 (draft, head 5e6e82e102d0) says retrieval adds no clause and removes positional k at a later major. This RFC adds retrieve and rejects positional k in edition 3. #792 (draft, head c5d33c2931a1) removes search, match_text, and fuzzy at the next major. This RFC puts those names in the working registry and then stops incompatible changes. State that those names remain aliases, or allow a registry row to be removed.

#884 (ready, head 24b60eba9e37) allocates a minor and an explain bump. Its base (2.2, explain 4) is stale. main is already (2, 3) and explain 7. That RFC is a minor.

The alternatives line "three open RFCs already schedule four majors" matches #805 (two majors), #792, and #793. The line "Each is a break" also counts #884. Record, in this decision log, the grammar sentences each of those drafts must remove. Leave supersedes empty while they still own the semantics. The RFC process requires that shared dependency to be explicit.

Optional

  1. Cite the Compatibility wording of the shared-expression RFC beside the Summary sentence, or quote the Summary and name that section.
  2. ea895066 is behind current main. The four files above match local e7d95ae. Name the merge base when this RFC is accepted. State the bump base as (2, 3) and explain 7.
  3. State what boot does when the IR proof refuses. A parse failure quarantines the graph today. An in-memory rewrite does not update the cluster bundle, so a warning "once per query" repeats on every boot until an operator runs queries migrate. Say whether the warning is once per process.
  4. L… matches neither the query catalog (Q, T, P) nor schema lint (OG-…). Name the catalog that owns the count { } > 0 and count { } = 0 lint.
  5. The heading "Operators" introduces the boot window. Use a heading that names the window. The engine-catalogue RFC decides five catalogue types. Attribute @Type, @Property, @Edge, @Interface, @Index, @Function, and @View to this RFC's reservation.

Liability

The one break removes repeated majors, a second spelling of one traversal, and one grammar production per function. That reduces long-term liability when later RFCs only fill a slot or add a registry row.

This RFC adds one release with two grammars, a rewriter, a boot rewrite, a function registry, an annotation registry, a working having, one explain and bound-plan advance, and additive envelope fields. Five more breaks of the same shape would rebuild the accretion this RFC is written to stop. The freeze succeeds when #792, #793, and #805 remove the majors they still schedule.

A frozen F64? score, and an equality proof that rejects every ranking query, add liability. They publish a contract the engine does not implement, and they block the evidence gate this RFC requires. The column type, the file rule, schema annotation punctuation, and edge direction already have owners: score_type, query_file, annotation_kwarg, and member direction. New registries are justified for functions and annotations that are still separate grammar productions. They are a second copy if they also own those four contracts.

One grammar is the right priority over a permanent left-arrow spelling and a long deprecation shim. That trade fits a pre-stable language. Keep the published F32 score and a proof the corpus can pass.

Invariant 9 (typed structures) is stronger when a registry and a named score column replace a grammar keyword. It is weaker while the frozen column type is wrong. Invariant 8 requires the rewriter to refuse an unprovable edit, and requires a loud boot outcome when the proof fails. Invariant 13 binds the grammar to the rewritten corpus and to the competence instrument. Those gates are the right evidence. They fail while findings 2 and 3 stand. No deny-list item is added when having is a Filter and the rewriter is a compiler pass. No Lance API is written. The column type still freezes the Lance f32 distance.

Validation

GQT regression proof does not apply. This pull request changes documentation only. It changes no query result, error, or plan.

Limits: the 37 of 45 competence figure is copied from RFC 0047. I did not re-run that instrument. The size of the stored-query fleet is the RFC's figure. I did not measure the boot rewrite. These docs checks are local. I did not use a CI run on this commit as evidence.

How to revise

Revise the freeze table, the rewriter table, the file rule, the annotation row, and the decision log. Each slot should name the owner that already holds the contract. The open drafts should remove the grammar sentences this edition takes, in this decision log, before (3, 0) is the single major.

Inline comments (RIGHT side of docs/rfcs/2026-10-28-gq-3-grammar.md)

Line 273: Blocking. TypedExpr::score_type returns F32 for nearest and bm25, and F64 only for rrf (crates/omnigraph-compiler/src/query/typecheck.rs). The comment there says Lance writes F32 scores and distances. A projected score in crates/omnigraph-gqt/cases/issue_640_search_score_projection.gqt is F32. Returned rows are the window or the match set. A row outside the window is absent. A non-match is absent. Lance 13 distances are f32. Freeze F32 for these two arms and F64 for rrf. score_type and score_column already own this contract. Keep the frozen type equal to the type score_type publishes, so a later Lance distance type is a language break only when that owner changes.

Line 284: Blocking, with the row above. The overfetch ladder widens candidates so a graph or property filter can still fill limit (docs/user/search/index.md). A distance predicate after the window can return fewer rows than limit. That is a new observable contract. T32 already refuses a retrieval under an aggregate, so a query with both is unspecified. State the having and limit interaction, or keep a score having as a slot until the ladder is specified to see the predicate.

Line 299: Blocking. IRExpr derives Eq, and the variants differ. Today the leading order key is IRExpr::Nearest, Bm25, or Rrf, so search_node can build LogicalNode::Nearest, TextSearch, or RankFuse (crates/omnigraph-planner/src/optimizer.rs). After this rewrite the order key is a property access of the arm name. The trees differ, so the proof refuses every ranking query and the corpus gate cannot pass. Compare the logical nodes after both editions lower. Treat the generated arm name as a projection alias. lower.rs and those logical nodes already own this. A list contains rewritten as in, and a string contains(), can stay equal when both lower to the same Binary operator.

Line 307: Blocking. The traversal section says the fix prints -> with the ends in schema order (a swap). This row always emits -> and leaves the variables in the written order. An incoming pattern then changes Direction on the expand operator, and the equality proof in the paragraph above fails. Also add rows for $a * $b, $a <*> $b, and $a "in" $b. EdgeSelector::Wildcard includes an edge in either direction between the two types. The form that keeps both orientations is $a -*- $b. The freeze table's $a -*-> $b keeps one orientation. The sentence at line 319 ("unchanged text") leaves the omitted spellings unparsed. Line 483 says a traversal is one hop. Hop bounds remain ({1,3} is in this table). The accurate claim is that one clause is not a path chain. Line 637 should name every removed spelling: the bare triple, undirected angle brackets, the edge binding, alternation, the wildcard, the quoted edge, and the left arrow.

Line 366: Blocking. query_file today is show, branch, commit, load, explain, or many query_decls, plus statement_trailer and missing_param_list (query.pest). This production accepts one undefined statement, or declaration*, and declaration* also matches an empty file. That omits the statements the summary says stay, and it omits the missing-() diagnostic the competence gate measures. Extend the current query_file rule. Keep missing_param_list.

Line 253: Blocking. The compiler accepts @description and @instruction only (query.pest). @example and @if_commit are absent. --if-commit is a request precondition. Mark those two as slots, or specify them as new behavior. The later note that the writes RFC must choose one owner for @if_commit and the request field is the right open question, and it conflicts with "the typed four work" here. annotation_args (line 558) has no production. Schema annotations already use ident = literal (annotation_kwarg in schema.pest). Call options use :. Define annotation arguments with =. A grammar that applies : to @embed("text", model="…") refuses text a .pg file accepts today.

Line 132: Blocking. #884 allocates a minor and an explain bump. Its pins (2.2, explain 4) are stale: main is (2, 3) and explain 7. It is a minor. The four majors are #805 ((3, 0) then (4, 0)), #792, and #793. This RFC also takes (3, 0). #805 still assigns that number to the infix-to-call change. #793 says retrieval adds no clause. #792 removes search, match_text, and fuzzy at the next major, while this registry keeps those names and then stops incompatible changes. State that those names remain aliases, or allow a registry row to be removed. Record in the decision log the grammar sentences each draft must remove. Leave supersedes empty while those drafts still own the semantics.

ragnorc commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Recommendation: revise.

This review covers head 7c44b23d683921d7ed84104615f3525d0d33b441. It reviews the delta 6dc6315d..7c44b23d only. The earlier comment on 6dc6315d still stands where this delta does not change the text.

Why

Each GQ feature has reserved a word, added a call, or advanced an explain version. Open drafts still schedule their own majors. One edition that reserves the missing positions costs less, when those positions match the engine that already exists.

What this delta changes

The triple stays. $p worksAt $c remains the directed spelling, with the source on the left. <e> remains the undirected spelling. An arrow is a recognised error whose fix prints the triple. The evaluation is committed beside the RFC. Catalogue type names and an @-prefixed catalogue edge are reserved slots, so a large schema can be queried in GQ.

Keeping the triple is the right cut. The arrowhead adds no fact. Position and the schema already carry direction. The rewrite of every traversal was the largest mechanical edit in the break, and this delta removes it.

Earlier findings

  1. Still open. The nearest column and the bm25 column are still F64?, null outside the window or null for a non-match. TypedExpr::score_type returns F32 for both, and F64 only for rrf (crates/omnigraph-compiler/src/query/typecheck.rs). The comment there says Lance writes F32 scores and distances. Lance 13 distance tables are f32 (lance-index 13.0.0 vector/pq/distance.rs). A projected score in crates/omnigraph-gqt/cases/issue_640_search_score_projection.gqt is F32. Returned rows are the window, or the match set. A row outside the window is absent. having { $d.vec < 0.6 } is still described as the same Filter the overfetch ladder already applies. The ladder widens candidates so a graph or property filter can still fill limit (docs/user/search/index.md). A distance predicate after the window can return fewer rows than limit. The evaluation repeats the same F64? null-outside-the-window column (§9.5).

  2. Still open. The rewriter still refuses a write unless the edited text compiles to a typed IR equal to the original. The new lowering paragraph says the leading order key becomes an arm reference, and search_node finds that reference where it found a call. Eq on IRExpr compares Nearest, Bm25, or Rrf with a property access of the arm name. Those trees differ, so the proof refuses every ranking query. Compare the logical nodes after both editions lower. Treat the generated arm name as a projection alias.

  3. Partly resolved. The rewriter table no longer rewrites a triple into an arrow. The freeze list names $a knows $b, $a <knows> $b, $a $e:knows $b, the alternation, $a * $b, and $a "in" $b. Hop bounds stay on traversal. The compatibility list no longer says the break removes the triple. Two holes remain, and they are blocking. See new finding 1.

  4. Still open. file is still edition? ~ setting_stmt* ~ (statement | declaration*). query_file today is show, branch, commit, load, explain, or many query_decls, plus statement_trailer and missing_param_list (query.pest). statement has no production. declaration* matches an empty file. The evaluation's own extension table (§7.2) names those statement kinds as the file-level ordered choice. Extend query_file. Keep missing_param_list.

  5. Still open. The annotations row still says the typed four work. The compiler accepts @description and @instruction only (query.pest). @example and @if_commit are absent. --if-commit is a request precondition. The one-spelling section still leaves @if_commit and the request field to the writes RFC. That sentence and "the typed four work" disagree. annotation_args is still used and has no production. Schema annotations use annotation_arg then ident = literal (schema.pest). Define annotation arguments with =.

  6. Still open. The heads are unchanged: rfc: amend the shared expression model with the operator vocabulary #805 49f436ce618b still assigns (3, 0) and (4, 0); rfc: search contracts on the shared expression model (RFC 0048) #793 5e6e82e102d0 still says retrieval adds no clause; rfc: analyzed lexical search on the shared expression model #792 c5d33c2931a1 still removes search, match_text, and fuzzy at the next major; rfc: polymorphic types with interfaces, unions and polymorphic edges #884 24b60eba9e37 is still a minor whose pins (2.2, explain 4) are stale. main is (2, 3) and explain 7. The decision log does not record the grammar sentences those drafts must remove. supersedes is still empty.

Optional items from that comment:

  1. Resolved. The motivation quotes the Compatibility section of the shared-expression RFC.
  2. Still open. Code references still say main ea895066. Those four files are unchanged from that commit through main 17557824. Name the merge base when the RFC is accepted. State the bump base as (2, 3) and explain 7.
  3. Still open. The text still does not say what boot does when the IR proof refuses. A parse failure quarantines the graph today.
  4. Still open. L… still matches neither the query catalog (Q, T, P) nor schema lint (OG-…).
  5. Partly resolved. The catalogue names and the @ edge selector are now slots, and the engine-catalogue RFC's schema extension is named as their owner. The heading "Operators" still introduces the boot window.

Blocking

1. An incoming triple is legal edition-2 text, and the swap fails the equality proof

$c: Company worksAt $p typechecks today when WorksAt runs from person to company. resolve_member selects Direction::In. crates/omnigraph-gqt/cases/issue_659_compiler_scopes.gqt contains $c worksAt $p and $c (worksAt | employs) $p under edge WorksAt: Person -> Company. The same shape is test_edge_selection_resolution_and_scope_issue_659: one member is Direction::In, the other is Direction::Out.

This RFC makes that triple T5, and the fix swaps the ends. The rewriter table does not contain the swap. "Everything not in the table is unchanged text" therefore leaves the corpus clause in place. Edition 3 then rejects it. The gate that requires a green corpus cannot pass.

The swap also fails the IR proof when the rewriter does apply it. The original lowers to Direction::In with company at the source variable. The swapped text lowers to Direction::Out with person at the source variable. The pairs of nodes are the same. The typed traversal is not. Finding 2's equality check refuses the edit.

The compatibility sentence says every edition-2 text other than query-for-write, the infix string operators, and positional rrf parses unchanged. This T5 is a further break, and the list does not name it.

arrow_traversal is a separate rule. clause tries traversal and does not try arrow_traversal. A pest rule that the start rule never reaches does not recognise $p -worksAt-> $c. Put the recognizer in the clause choice, before traversal, so <knows> stays the undirected form.

<e> over an asymmetric edge is T22 today (test_undirected_traversal_rejected_on_asymmetric_edge). The freeze row says the traversal is unchanged from edition 2, and the evaluation's break table says "today's semantics" (§11.6). The prose then narrows T22 so <owns | ownedBy> resolves per member. That is a semantic change. State it as a change, and add the rewriter or the proof that keeps issue 659's rows.

Keep both written orders for an asymmetric edge. They are one meaning, and the type checker already records which orientation each member takes. The one-spelling rule still removes the arrow, the infix operators, positional k, and query around a write. A later canonical spelling can be a formatter rule once the proof compares resolved members, not raw variable order.

2. The evaluation still specifies the arrow rewrite this delta withdrew

The RFC cites the evaluation as its evidence. These passages still tell an implementer to rewrite triples into arrows, or they keep decisions the RFC has dropped:

  • §9.4's sample file uses -supports->, -about->, and -$e:supports->.
  • §9.9 still lists the arrow spelling as one of the four incompatible items, and the next sentence still rewrites $p worksAt $c to $p -worksAt-> $c.
  • §11.1's diagram still labels the break "(arrows, mutation, retrieve".
  • §11.2 stage X still contains the arrow spelling. The agent outcome is still "direction visible". The withdrawal note in parentheses does not replace that row. The same section says the stage contents stand.
  • §11.6's break paragraph still gives the rewriter $p worksAt $c → $p -worksAt-> $c.
  • §11.6's freeze list still includes contract. This RFC removed that declaration on 2026-10-29. The same row says Branch and Commit parameters bind. The RFC says they are slots. The envelope row bumps the HTTP contract from 0.14 to 0.15. The RFC says the HTTP contract does not change.

§9.3, §13.3, and §14.2 record the withdrawal. The operative rewrite, the sample, and the stage table do not. One source has to win. Make the evaluation match the RFC's freeze list, rewriter table, and compatibility list. A historical sentence can say the first draft chose the arrow. It cannot remain the instruction for queries migrate.

3. A catalogue retrieve is not a query over an ordinary table

The new property says @Type, @Property, @Edge, @Interface, @Index, @Function, and @View are bound, traversed, searched, and counted like user data. The traversal section says match { $t: @Type } retrieve { $t.lex: bm25($t.instruction, $q) } is an ordinary query over an ordinary table, and that the grammar does not distinguish it.

The accepted engine-catalogue RFC says the five catalogue types are derived at query time, never stored, and never part of schema identity. "Writes, loads and text or rank search over catalogue types are refused; catalogue access never calls Lance." The user guide repeats the refusal: catalogue rows have no index (docs/user/queries/catalogue.md). There is no @instruction column on those types. There are no catalogue edges.

Reserving the seven names and the @ edge selector as slots is coherent. Calling the search an ordinary table conflicts with that accepted contract. Rank search needs an index or a scan the catalogue does not have. Five more features of this shape would grow a second schema store beside the derived catalogue.

Say the slots parse and refuse until a catalogue extension amends the rank-search refusal. That extension names the mechanism, and it does not add a Lance dataset for rows the catalogue RFC keeps derived. bm25 stays on user tables, where the index exists.

Optional

  1. RFC 0047 reports 45 of 45 tasks correct and 8 of 45 first attempts refused, all for a missing (). It does not report a direction result. "No direction error" is an inference. CypherBench's figure in the evaluation is 9 of 55 error marks for gpt-4o, not one failure in six. Cite the 0047 sentence as written.
  2. updated is 2026-10-29. The decision log has a 2026-10-30 entry. Frontmatter is the date authority.
  3. < and > are already comparison operators (comp_op in query.pest). The ordered choice of traversal before filter already keeps $a <knows> $b a traversal. The function-registry work does not introduce that operator.

Liability

The triple decision removes a second spelling and the largest rewrite. That reduces long-term liability. The arrow recognizer is a small diagnostic, and it belongs in the existing clause choice.

The delta adds a 4,194-line evidence file. That file is justified as the record of the comparison. It adds liability while its break instructions disagree with the RFC. Two readers can implement two grammars. One file has to be the decision. The RFC is that file. The evaluation should follow it.

Unchanged liability from the earlier comment: a frozen F64? score, an equality proof that rejects every ranking query, a file rule that drops live statements, and (3, 0) shared with drafts that still schedule their own majors. The incoming-triple T5 adds a break the compatibility list hides, and the proof cannot show the swap preserves the plan. Catalogue search described as an ordinary table would duplicate the derived catalogue.

Invariant 9 is stronger when one registry replaces a grammar keyword. It is weaker while the score type and the incoming-triple plan disagree with the type checker. Invariant 8 requires the rewriter to refuse an unprovable edit. The swap is that edit. Invariant 12 and the catalogue RFC keep one derived catalogue. A searchable catalogue table would be a second source. Invariant 13 binds the grammar to the rewritten corpus. That gate fails while findings 2 and 3 from the earlier comment, and finding 1 here, stand. No deny-list item is added by a Filter or a compiler pass. No Lance API is written. The column type still has to freeze the F32 value score_type publishes.

Validation

  • Head reviewed: 7c44b23d683921d7ed84104615f3525d0d33b441. State: open, ready for review. Rechecked immediately before this comment. Reviews and inline comments: none. Issue comments: the earlier review on 6dc6315d only.
  • Related pull requests still open at the heads named above: rfc: amend the shared expression model with the operator vocabulary #805, rfc: analyzed lexical search on the shared expression model #792, rfc: search contracts on the shared expression model (RFC 0048) #793, rfc: polymorphic types with interfaces, unions and polymorphic edges #884.
  • python3 scripts/check-pr-title.py --title "rfc: the GQ 3 grammar, one break and the slots it freezes" → PASS.
  • Detached checkout of this head, after python3 -m pip install --user -r scripts/requirements-docs.txt: python3 scripts/check-docs.py → Documentation OK (436 Markdown files checked). The new asset is outside docs/rfcs/*.md, so the registry check does not treat it as an RFC. Links and the RFC row pass.
  • bash scripts/check-agents-md.sh → AGENTS.md ↔ docs indexes OK (66 links, 62 docs).
  • git diff --check 6dc6315d HEAD → exit 0.
  • typos 1.50.1 on the RFC, the evaluation asset, and .typos.toml → exit 0. EDN and FO occur in the asset as the intended tokens.
  • Code inspection at main 17557824 of query.pest, schema.pest, typecheck.rs, codes.rs, the issue 659 tests and GQT case, issue_640_search_score_projection.gqt, the engine-catalogue RFC, and Lance 13.0.0 lance-index distance tables. query.pest, schema.pest, GQ_LANGUAGE_VERSION, and EXPLAIN_VERSION have no commits between ea895066 and 17557824.

GQT regression proof does not apply. This pull request changes documentation only. It changes no query result, error, or plan.

Limits: these docs checks are local. I did not use a CI run on this commit as evidence. The path classifier's docs/*.md arm does not cover docs/rfcs/ or .typos.toml, so this pull request is outside the docs-only skip. I did not run the workspace test graph. The 37 of 45 figure is copied from RFC 0047. I did not re-run that instrument. I did not re-measure the CypherBench figure from the paper image.

How to revise

Keep the triple. Revise the incoming-triple rule so edition 2's accepted orders still typecheck, or specify a proof that does not require the swapped IR to be equal. Wire arrow_traversal into clause. Make the evaluation's sample, stage table, and rewriter paragraph match the RFC. Describe catalogue search as a slot that waits for an amendment of the rank-search refusal. The earlier open items (score type, IR proof for rankings, file, annotation arguments, and the other drafts' majors) are unchanged by this delta and still block.

ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Nov 2, 2026
Throwaway prototype of the GQ 3 grammar: an edition-3 front end selected by
`gq 3;`, every freeze-list row and slot, a function registry, `retrieve`,
`having` and `limit $k` through the planner onto Lance, the edition-2
rewriter with its IR-equality proof, and tests that stress the one-break
claim against the corpus, generated queries and the open RFCs' forms.
Findings: docs/spikes/gq3-1202.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ragnorc

ragnorc commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Spike report: the GQ 3 grammar (this RFC at 7c44b23d)

Overall verdict

Correct with amendments. No blocker found that forces a second grammar break, but the claim as written is stronger than the evidence, and five non-grammar contracts must be settled before the freeze or they become the second break.

  1. The one-break claim holds for the grammar. I wrote 60 later capabilities (from this RFC, its evaluation, rfc: analyzed lexical search on the shared expression model #792, rfc: search contracts on the shared expression model (RFC 0048) #793, rfc: amend the shared expression model with the operator vocabulary #805, rfc: polymorphic types with interfaces, unions and polymorphic edges #884, rfc: schema namespaces and multi-file schemas #1015 and the Lance primitives) as the queries they need. None changes the meaning of a well-typed edition-3 query. 16 parse into a slot and are refused with its owner, 14 are registry rows or options, 5 already work, 24 need a new grammar production, and 1 already parses with another, ill-typed meaning.
  2. The "every later capability has a reserved slot" claim does not hold. The 24 new productions include pattern-scoped writes, which the Motivation says have slots, plus unions and is T (rfc: polymorphic types with interfaces, unions and polymorphic edges #884), qualified edges, use and namespace blocks (rfc: schema namespaces and multi-file schemas #1015), arithmetic (weighted fusion, computed writes), partial objects, at $commit, nulls last, multivectors and new base types. Each one starts at a token edition 3 already refuses, so each is additive. Compatibility comes from PEG ordered choice, contextual keywords, $-variables and the @ namespace, not from the slots. The slots buy a committed spelling and a better diagnostic, not compatibility. The RFC should say so and drop the sentence that a later capability is "a registry row, a plan node, or the filling of a slot".
  3. The rewriter and its IR-equality proof work, against the review's prediction, provided an arm reference is resolved to its call before IR is built. 1,818 GQT declarations and 2,968 generated reads migrate and compile to identical typed IR. A swapped incoming triple also lowers to identical IR (4 of 4 corpus cases and a negative control).
  4. Five contracts would break later if frozen as written, or are scheduled to break by other RFCs:
  5. The RFC's pest sketch has defects that would make the frozen grammar refuse its own examples or today's text (amendments G1 to G8). All are small fixes, and the spike's grammar applies them.

Amendments

  • A1, score columns. Freeze what the engine publishes. On Lance that is a non-null Float32 named <var>._distance or <var>._score (F64 for rrf), not F64? named after the arm. Alternatively, decide now that edition 3 renames the column to <var>.<arm>, and list that as part of the break: 2 GQT projections change name.
  • A2, having over a score. State that it means "the top limit rows of the population passing the predicate". That is what the engine already does: the overfetch ladder widens until limit rows pass, and it equals Lance distance_range. Freeze it in edition 3.
    • Under rrf, an arm's own score cannot be projected or filtered today (T33), because the fusion carries only the fused column. "Scores are values" therefore needs RankFuse to carry each arm's column (engine work), or having over a fused arm stays a slot.
    • having also needs its own type rule. Today's Boolean-return rule (T58) refuses a group key beside an aggregate (count($d) >= 2 and $p.name != "x").
  • A3, rfc: search contracts on the shared expression model (RFC 0048) #793's refusals. Move them into edition 3, or have rfc: search contracts on the shared expression model (RFC 0048) #793 drop them. Edition 3 accepts duplicate arms, arms on two bindings, and a nearest arm in an aggregate query; refusing them afterwards is a break.
  • A4, registry rows are never removed. rfc: analyzed lexical search on the shared expression model #792 removes search, match_text and fuzzy at "the next major", and its T27 tightening refuses bm25 over @index without @analyzed. Under one break, either rfc: analyzed lexical search on the shared expression model #792 lands those in edition 3 or the rows stay for good, with the formatter canonicalising to match_terms. Record the choice in both RFCs. rfc: amend the shared expression model with the operator vocabulary #805's (3, 0)/(4, 0) numbering also has to go, as the review says.
  • A5, the edition-3 incoming-triple refusal (T5) is a break the compatibility list omits. The rewriter table needs a swap row. With it, the proof passes (the 4 corpus cases inside not/exists/count blocks in issue_659_compiler_scopes.gqt). The RFC must also say what a directed alternation with mixed member orientations is. It resolves per member today; the "written against the schema" rule does not say.

Grammar fixes (the spike's grammar applies each; tests pin the RFC's literal rule failing)

  • G1. call_args: positional ~ ("," ~ positional)* ~ ("," ~ option)* never parses an option after a positional (rrf($a, $b, k: 60)), because PEG takes k as a bare-name positional and does not backtrack. Fix: positional = { !(ident ~ ":") ~ expr }.
  • G2. binding with arguments: $p: Person followed by a parenthesised filter on the next line (legal edition-2 text) is read as view arguments, a silent change of meaning. Fix: the argument list must touch the name (Name().
  • G3. undirected_edge = "<" ~ edge_sel ~ ">" drops today's <knows | likes>, which the typed edge alternation RFC accepted. Fix: admit edge_alternatives inside < >.
  • G4. Deleting date_lit/datetime_lit breaks [date("…"), …] list literals (issue_801_in_predicate_over_a_list.gqt). Fix: a list item is a literal or a constant call.
  • G5. count_call admits distinct only after count, so sum(distinct $x) changes from T57 to a parse error (issue_954_count_distinct.gqt). Fix: any aggregate name, with T57 kept in the type checker.
  • G6. annotation_args is undefined. Use = for named arguments (the .pg spelling), allow any order and several positionals (@valid(a, b), @analyzed(analyzer="…")), meta-fields and variables (@if_commit($c)). The evaluation's @budget(rows: 10) then does not parse; one spelling has to win, and = is the one stored .pg text uses.
  • G7. prop_access is one level, so the frozen meta-fields @distance/@score "of a retrieve arm's column" have no spelling ($d.vec.@distance). Either freeze a path rule or drop the two names.
  • G8. The arrow recognizer must be in clause before traversal (the review is right). Some RFC examples do not parse as intended:
    • $p: Person <owns | ownedBy> $d: Doc is a parse error. Traversal endpoints are bare variables.
    • $t: @Type @property $p: @Property and $e: @Edge @from $t parse as a binding, a bare @… filter and a second clause, not a traversal.

Per-claim results

Frozen decision or claim Result Evidence
One incompatible change, then only additive ones Holds with a change (A3, A4, and the score contracts A1/A2) later_tests.rs: of 60 later forms, 0 change the meaning of an accepted well-typed query. #793's refusals and #792's removals are the remaining scheduled breaks (rfc_0048_refusals_against_edition_3: all three accepted).
Every later capability has a reserved slot Does not hold (wording) 24 of 60 need a new production (later.tsv: pattern-scoped update/delete, invalidate, unions, is T, Person($id), $x: *, qualified edges, use, namespace, arithmetic, k 200, rerank stage, partial objects, limit 3 per, at $commit, nulls last, offset, multivector). Each starts at a token edition 3 refuses (Q001), so each is additive. One ($p: Person at date(…)) already parses as a binding plus two bare filters and is refused only by the type checker. Arithmetic $x * $y in match meets the wildcard traversal $a * $b and needs a lookahead (arithmetic_meets_the_wildcard_traversal).
mutation for writes; query with a write body refused with the fix Holds mutation_keyword_and_the_wrong_keyword_refusal; engine mutation_keyword_writes_and_query_keyword_refuses_a_write (T62 with a machine-applicable edit).
Triple traversal unchanged; <e> for either orientation Holds with a change (G3, A5) the_triple_is_unchanged_and_arrows_print_the_triple; the RFC rule refuses <a | b> (rfc_undirected_rule_drops_the_unparenthesised_alternation). <owns | ownedBy> is still T22; the RFC's T22 narrowing is a semantic change I did not implement.
Arrow forms are parse errors whose fix prints the triple Holds (with G8) Q008 for -e->, <-e-, -e-, with binding and bounds; the fix is computed without the schema and preserves meaning.
A triple written against the schema is T5 with the swapped fix Holds with a change (A5) an_incoming_triple_is_refused_in_edition_3. Corpus: 4 edition-2 declarations become refusals. The swap keeps typed IR equal (swapping_an_incoming_triple_keeps_the_typed_ir, with a same-type negative control showing the comparison detects a real swap) and rows equal on Lance (an_incoming_triple_and_its_swap_return_the_same_rows).
retrieve { } binds typed score columns Holds with a change (A1, A2, G7) Engine retrieve_matches_the_edition_2_ranking_on_lance_indexes: nearest, bm25, rrf(k: 60), a match-scoped and a traversal-scoped ranking return the same rows as edition 2. arm_columns_keep_todays_score_types_and_names: non-null Float32, named d._distance/d._score, not F64?. Under rrf an arm is not a value (T33).
order sorts values; leads with an arm or rrf over arms Holds T65 for a ranking written in order or an arm order does not read; T54 and T17 unchanged.
having filters values Holds with a change (A2) having_filters_groups_and_scores: groups by alias or aggregate; a score threshold returns the top limit passing rows through the existing ladder (a lower bound > 0.01 returns d2, d3, d5, not the 2 a post-window filter would give). Group key beside an aggregate refused (T58 inside T67). Explain: Filter above Sort, below Limit (explain_*.json).
Functions are generic calls over one registry Holds with a change (G1, G4, G5, A4) calls_resolve_against_the_registry, a_new_function_is_a_registry_row_not_a_grammar_rule (a row added in a test resolves, no grammar change); fuzzy's third positional must become max_edits:, a row the rewriter table lacks.
Reserved set closed at the six logic words Holds every_contextual_keyword_stays_an_identifier: 54 keywords legal as a property after a dot, an edge name, a return alias and a bare mutation property; the six refused as aliases.
limit $k Holds limit_binds_a_parameter (k = 1, 2, 4 on Lance; negative refused at bind); explain shows Nearest k: 2 from $k. Plan acceptance needed a change (it compares the written limit).
null only as an assignment value Holds with a change null_is_legal_only_as_an_assignment_value. The RFC also admits null in prop_match, which is the read binding ($p: Person { age: null }), contradicting "no literal in a read"; the spike refuses it.
Generic @name(args) annotations; @description, @instruction, @example, @if_commit typed and working Holds with a change (G6) annotations_are_generic. @example and @if_commit have no mechanism in the compiler or engine; the spike makes them slots.
Reserved declaration kinds view, batch Holds every_slot_parses_and_refuses_with_its_owner (T60, owner in fix).
optional { }, block aggregates as values Holds Same test.
View binding $x: Name($arg) Holds with a change (G2) rfc_binding_rule_swallows_a_parenthesised_filter.
returning Holds as a slot Same test.
Meta-fields @type_id, @distance, @score Holds for @type_id; with a change for the other two (G7) Same test.
Catalogue names and the @ edge selector Holds as slots, with G8 The review is right that retrieve over @Type contradicts the accepted catalogue RFC; the spike refuses it as a slot.
Namespaced type references a::b::C Holds for types #1015 also needs qualified edge names, use and namespace blocks (new productions). RFC type_ref admits crm::@Operator, which #1015 makes a syntax error.
Schema roles Vector(n, distance=…), @analyzed, @valid Holds with a change (G6) later_schema_forms_against_the_one_grammar: all three parse with the spike's annotation_args; none parses with today's schema.pest rules.
One grammar for schema and queries Holds 432 GQT schemas and the 5 repository .pg files parse under the one file rule. The RFC does not say what a query source containing a schema declaration means; the spike refuses it as a slot.
file keeps today's statements Holds with a change (review finding 4) The spike keeps show/branch/commit/load/explain, settings and missing_param_list (Q002 with the same fix).
Optional gq 3; edition line Holds edition_line_selects_the_grammar.
Rewriter: byte-range edits, IR-equality proof, idempotent Holds with a change: the arm reference must resolve to its call before IR, the table needs the fuzzy and swap rows, and the gate must allow expected-code updates Corpus (2,609 sources: GQT query/mutate sections, user/dev guide and skill code blocks, .gq files): 1,818 declarations IR-equal, 0 differing, 611 statements unchanged, 43 doc blocks parse-only (no schema), 2 renamed result columns. Of 132 expect-error steps, 129 keep their code and 3 change (T33→T65 twice, T32→T65), so "zero hand edits" fails on expectations unless the gate allows them. Fuzz: 2,968 generated reads IR-equal after migration, all idempotent.
Plan acceptance gains the arm and having Holds, and it is required work Without it the validator refuses having (predicate origin, order, row cut) and limit $k (lowering). RFC 0047's exact fragment, a closed and versioned contract, also classifies a having/limit $k read as a member and then finds no derivation (the server test hit this); the spike excludes both forms from the fragment. Its version must move, or the fragment must exclude them, in edition 3. The spike's exemptions are stubs.
Planner maps onto Lance, filters and limits pushed down Holds explain_retrieve_having.json: topic = "db" in the Lance scan filter, Nearest k: 2 bound from $k, the scan and search fused physically, having as a typed Filter with the threshold cast to F32.
HTTP contract unchanged Holds for reads spike_gq3_edition_3_stored_read_boots_and_serves: an edition-3 stored read with retrieve, having and limit $k boots and returns today's envelope, including ranking.score_columns. The CLI runs edition-3 text unchanged and prints the new diagnostics with code, position and fix (target/spike-gq3/cli_demo.txt).
Parse and type-check cost Not a design concern Parse, check and lower over 1,808 sources × 20 rounds (debug build): edition 2 5.95 s, edition 3 8.11 s (1.36×). The spike type-checks having twice and re-parses settings; this is not a measure of an optimised implementation.

Lance primitive table

Lance 13.0.0 (all lance* crates in Cargo.lock), DataFusion 54.0.0; paths under lance-13.0.0/src/ unless noted. Exposed: the frozen grammar reaches it. Row/slot: a later release reaches it with a registry row, call option, slot or annotation argument. Plan: a planner change behind existing syntax. No slot: a new (additive) production.

Primitive Engine uses it GQ 3 How
Scanner::filter/filter_expr (scalar-index routing) dataset/scanner.rs:1705,1776 yes exposed match filters, binding matches
Scanner::project :1564 yes exposed return
Scanner::limit(limit, offset) :1956 internal exposed (limit n, limit $k); offset: no slot offset is refused by design
Scanner::nearest :1990 yes exposed nearest arm; k = limit, widened by the ladder
Scanner::prefilter :1622 yes plan eligibility placement
Scanner::distance_range :2117 no plan having over a lone nearest arm already has its semantics (A2); a pushdown would remove the ladder's re-search
nprobes/minimum_nprobes/maximum_nprobes :2133-2182 max only (set ann_nprobes) row options on nearest
ef, refine, fast_search, distance_metric, approx_mode :2191-2275 no row options
use_index(false) :2263 yes (ladder) row exact: true
full_text_search (MatchQuery, fuzzy) :1746; lance-index scalar.rs:116,127 yes exposed search, match_text, fuzzy(…, max_edits:), bm25 arm
FTS BooleanQuery should/must_not must only row query-valued functions; needs a query value type in the registry
PhraseQuery/slop, BoostQuery, MultiMatchQuery/BM25F, Operator::And, prefix length, max expansions, wand_factor no row phrase(…, slop:), options, variadic rows
BM25 K1/B lance-index scalar/inverted/scorer.rs:73-74 constants n/a not configurable in Lance
nearest and FTS in one scan refused by Lance :3354 exposed as rrf fused in the engine
minhash_search :1757 no row + annotation an arm plus an index argument
Scanner::aggregate :1785 no plan behind today's aggregates
count_rows(filter) dataset.rs:1705 unfiltered only plan behind count
Scanner::order_by with nulls placement :2241 internal exposed (asc/desc); nulls: no slot nulls last is a new production
take_rows/take_blobs/read_blobs yes n/a Blob API
BTree yes exposed @index, @key
Bitmap, LabelList (array_has_any/all), NGram, ZoneMap, Bloom, FM index no annotation + row @index(kind="…") parses only with G6's annotation_args (today's .pg constraint rule refuses it); has_any, regex_match are rows
Inverted index parameters (tokenizer, stem, stop words, positions, folding) scalar/inverted/tokenizer.rs:105 default() slot (@analyzed) needs G6; list-valued options need lists in annotation arguments; positions change the RFC 0043 certificate
RTree/st_*, JSON index no (feature off) no slot new base_type productions
IVF_FLAT yes (L2) exposed @index on Vector(n)
IVF_PQ/RQ/SQ, IVF_HNSW_* no annotation @index(kind=…, m=…) with G6
DistanceType L2/Cosine/Dot/Hamming lance-linalg distance.rs:392 L2 slot for L2/Cosine/Dot (Vector(n, distance=…)); Hamming: no slot needs a binary vector type
Multivector no no slot [Vector(n)] is a parse error
merge_insert UpdateAll/Fail/InsertAll dataset/write/merge_insert.rs yes exposed keyed insert, update, load mode merge
WhenMatched::DoNothing/UpdateIf/Delete, WhenNotMatchedBySource, dedupe FirstSeen no no slot insert-if-absent, conditional upsert, sync loads need a statement modifier or load mode; @on_conflict(…) fits per declaration only
UpdateBuilder::set(col, sql) dataset/write/update.rs:151 no no slot computed assignment needs arithmetic and a relaxed T45
DeleteBuilder, InsertBuilder yes exposed delete … where, load modes
add/alter/drop_columns no (own Project/Merge) n/a schema apply
checkout_version yes (pins) slot (Commit parameter); at $c clause: no slot request --snapshot today
Branches __manifest exposed branch statements
Tags, restore, compaction, optimize_indices engine/CLI no slot new statement keywords (additive); Restore and cleanup_old_versions are deny-listed for graph tables
index_statistics/describe_indices partly slot (@Index) catalogue
delta() no (own feed) exposed branch diff
Dataset::sql, LanceTableProvider no n/a string-built semantics are deny-listed

What I could not verify

  • The competence instrument (the RFC's first gate). Its task set is outside the repository; I did not run it. The parser it needs is on the branch: gq 3; sources parse and refuse slots.
  • The edition-2 boot rewrite, cluster plan output, the release-after refusal, envelope fields, explain/bound-plan version bumps, named score columns in explain, and parameter defaults. None was built.
  • The fleet's stored queries. The corpus is the repository: GQT cases, the guides, the skill and .gq fixtures. 43 doc blocks have no schema, so they were checked for parse only.
  • Each of the 24 new productions. I showed each form is refused today, so adding it at its first novel token is additive. I did not implement them. Arithmetic needs a lookahead against $a * $b.
  • The RFC's T22 narrowing (<owns | ownedBy> per member). I did not implement it.
  • ANN behaviour of having under a real IVF index at scale. The Lance test graph is 6 rows, where the ladder's search is exact.
  • S3 and Azure, and release-build timing. Everything ran on local files and a debug build.
  • Workspace-wide test suites. I ran the spike tests plus the full compiler and planner suites (green). I did not run the canonical workspace test graph, GQT, clippy or DST. The QueryIR/QueryDecl field additions compile across the workspace.

Branch

Prototype, tests and evidence: spike/gq3-1202 (commit f63e4393, from main f728f01e). The repository's fork-only rule refused a branch on ModernRelay/omnigraph, so it is on the fork. The report is docs/spikes/gq3-1202.md. The run's artifacts are in docs/spikes/gq3-1202/. The code is throwaway; the edition-3 front end is crates/omnigraph-compiler/src/gq3/. Its "What was built" section lists what is real and what is a stub.

…-evaluation-5a029b

# Conflicts:
#	docs/rfcs/README.md
…ore columns, having semantics, both triple orders, registry rows kept, grammar fixes

ragnorc commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Recommendation: revise.

This review covers head 667642b137d52af72642f50c46d46f5269a0fb11. It reviews the delta 7c44b23d..667642b1 (95328a21, 667642b1, and the grammar-file result of merge 7e396a83). The spike report on 7c44b23d is evidence for this text. It is not a second decision.

Why

One grammar break is cheaper than the majors the open drafts still schedule, when the frozen forms match the engine that already exists. This delta takes the contracts the last review and the spike said were wrong, and it writes them into the freeze list.

What this delta changes

The score column is the non-null F32 (F64 for rrf) that score_type publishes. A row outside the window is absent. having over a score means the top limit rows of the population that pass the predicate. An incoming triple stays legal. queries fmt writes schema order. The arrow recognizer sits in clause. @example and @if_commit are slots. annotation_args uses =. query_file keeps today's statements. Pattern-scoped writes are new productions, not slots. The decision log names the sentences PR #805, PR #792, PR #793, and PR #884 must remove. The evaluation's header says the RFC wins where §9 or §11 differs.

How the text is organized

The RFC is the decision. The evaluation is the record of the comparison. The spike is the trial implementation. A slot parses and refuses with the owner RFC in fix. A registry row is a function or annotation the grammar does not name.

Impact

Seven of the eight blocking findings from the review on 7c44b23d are resolved, and one is partly resolved. All seven optional findings from that review are resolved. Three new defects still block the corpus gate or the traversal contract. Three optional notes remain.

Earlier findings

Blocking items the review on 7c44b23d left open:

  1. Resolved. The nearest column and the bm25 column are non-null F32. rrf stays F64. Rows outside the window are absent. having states the limit interaction (the top limit rows that pass). T67 is the type rule.
  2. Resolved. An arm reference resolves to its call before IR, so order { $d._distance } lowers to IRExpr::Nearest. That removes the Nearest versus property-access mismatch.
  3. Resolved. The rewriter no longer rewrites a triple. $a * $b, $a "in" $b, <e>, the binding, and the alternation stay. The new hole is score projections, in blocking finding 1 below.
  4. Resolved. query_file is today's rule plus edition?, with declaration* in place of query_decl*, and missing_param_list kept.
  5. Resolved. @example and @if_commit are slots. annotation_args is name = value or a positional value.
  6. Resolved. The decision log records the sentences the four drafts must drop. blocked_on requires those amendments before acceptance. supersedes stays empty. The four pull requests are still open at 49f436ce618b, c5d33c2931a1, 5e6e82e102d0, and 24b60eba9e37.
  7. Partly resolved. Both written orders of a directed triple stay legal, and arrow_traversal is in the clause choice before traversal. The formatter claim that the schema-order rewrite leaves the typed IR equal is still open. See blocking finding 3.
  8. Resolved. A retrieve arm over a catalogue type is a slot. The amendment that lifts the rank-search refusal must open no Lance dataset.

Optional items from that review:

  1. Resolved. Code references name main f728f01e. The bump base is (2, 3) and explain 7.
  2. Resolved. A source the proof refuses quarantines its graph. The boot warning is once per source per boot.
  3. Resolved. The query diagnostic catalogue gains the prefix L for lints.
  4. Resolved. The boot section is "The edition window at boot". Catalogue names name the engine-catalogue RFC.
  5. Resolved. RFC 0047 is cited as written (45 of 45, 8 of 45 refused, all for a missing (), no direction result). CypherBench is 9 of 55 gpt-4o error marks.
  6. Resolved. updated is 2026-11-01, the same day as the decision-log entry.
  7. Resolved. The traversal section says < and > are already comp_op, and traversal is tried before filter.

The compatibility citation from the review on 6dc6315d stays resolved.

Blocking

1. The fused score has no legal spelling, and the rewriter table drops live projections

order must lead with an arm or with rrf(arm, arm) (docs/rfcs/2026-10-28-gq-3-grammar.md, around the retrieve section). The same section says a ranking call in order or return is T65, and the fix is the arm. The refusals list repeats that a ranking call in order or return is T65. Under rrf, an arm's own column is not a value (T33). score_column names the fused value {var}._rrf (crates/omnigraph-compiler/src/query/ast.rs), and score_type types it F64. _rrf is not an arm.

crates/omnigraph-gqt/cases/issue_640_search_score_projection.gqt projects rrf(nearest(...), bm25(...)) as fused and expects F64. Edition 3 refuses the call (T65) and refuses each arm (T33). No row of the rewriter table produces a fused projection. The query has no spelling that the freeze list permits.

The table does rewrite return { nearest(...) } and return { nearest(...) as score }. It does not rewrite the sibling forms the corpus already runs:

  • return { bm25(...) } and return { bm25(...) as score } in the same GQT file (result column p._score, type F32)
  • return { bm25(...) > 0.0 as hit }, nearest(...) < 1.0, and rrf(...) > 0.0325 in crates/omnigraph-gqt/cases/rank_call_nested_in_a_boolean_projection.gqt
  • a repeated ranking key, as in order { nearest(...), nearest(...) } and order { rrf(...), (rrf(...) > 0.0325) asc }

The sentence after the table says everything not in the table is unchanged text. Unchanged ranking calls are T65. The corpus gate (zero hand edits, rewritten corpus green) cannot pass from this table. The spike report says 1,818 declarations compared equal, so the spike's rewriter rewrote more forms than the table lists. The table is the specification of that tool. Add a row for every live shape, including the fused projection. Say that rrf(arm, arm) in order is not T65. Say whether return { rrf($d._distance, $d._score) as fused } is the fused spelling, and type it F64.

2. T22 narrows by two different amounts

Three sentences in the RFC name three widths:

  • The freeze row says the traversal works with today's T22, and then says <e> over an alternation of opposed members resolves per member.
  • The refusals list says T22 keeps today's meaning and no longer fires for that alternation.
  • The traversal section says T22 narrows to an asymmetric edge whose endpoints are not typed well enough to fix the orientation.

The compatibility list names only the alternation. test_undirected_traversal_rejected_on_asymmetric_edge is $p <worksAt> $c and expects T22 (crates/omnigraph-compiler/src/query/typecheck_tests.rs). Both ends are typed. The broad sentence accepts that query. The narrow sentence keeps today's refusal. The undirected wildcard has a second T22 when the endpoint types differ (typecheck.rs, undirected wildcard). The broad sentence does not say whether <*> changes.

Pick one rule and use it in the freeze row, the refusals list, the compatibility list, and the traversal section. The evaluation's §11.6 traversal row still says "today's semantics". The header already says the RFC wins there. Make §11.6 say the same rule.

3. The formatter swap changes Expand, and IROp has no Eq

The proof refuses a rewrite unless the typed IR is equal, and the text says IRExpr, IROp, and their containers derive Eq and Hash. At this head, IRExpr derives Eq and Hash. QueryIR and IROp derive Debug and Clone only (crates/omnigraph-compiler/src/ir/mod.rs). The review on 6dc6315d recorded the opposite for IROp. That record does not match the source.

IROp::Expand stores src_var, dst_var, and EdgeMember.direction. $c worksAt $p under edge WorksAt: Person -> Company lowers to Direction::In with $c as src_var (resolve_member). The formatter's schema-order text lowers to Direction::Out with $p as src_var. Derived Eq, once it exists on IROp, refuses that pair. The one-spelling section says a formatter rewrite never changes the typed IR. That sentence is the line between queries fmt and the break. It is false for this Expand.

The spike report says a swapped incoming triple compared equal, with a same-type negative control. That comparator is not derived Eq on Expand. Name it. Compare resolved members in schema order, or compare the result rows. Do not say the trees are Eq. The same sentence should cover a traversal inside not, exists, and count, which is where issue_659_compiler_scopes.gqt writes the incoming triple.

The alternatives row "Correct a wrong triple silently" still answers with a typed error and a machine-applicable swap. The freeze list rejects that swap. Point the row at the formatter.

Optional

  1. docs/rfcs/assets/2026-10-28-gq-3-grammar-evaluation.md §10.1 still says projectable per-arm scores that are null outside the window are exactly §9.5's model. §9.5 now says the row is absent, not null. §9.3's GQ 3 cell still prints the arrow spelling. The withdrawal sits in the driver cell. The header covers §9 and §11. It does not cover §10.1. One sentence there can put the nullable column back into RFC 0048.
  2. The generic annotation_value is a literal, a variable, a meta-field, or an ident. It has no range bound. A pest 2.8.7 fragment of that rule accepts @valid(from, to) and @embed("text", model = "m") and refuses @range(age, 0..200) and @card(0..1). Today's cardinality and body_constraint (schema.pest) own those two spellings. The design section says the schema rules move unchanged except type_name, vector_type, and annotation. Say that cardinality and body_constraint stay, so "one annotation form" does not absorb 0..1.
  3. The sketch's type_ref is (ident ~ "::")+ ~ type_name | "@" ~ type_name | type_name. The same name in query.pest and schema.pest is the parameter and property type (String?, Vector(n), [String]). param uses type_ref_or_kind, and that rule has no production. A fragment of the new type_ref accepts Person, crm::Person, and @Type, and refuses String? and Vector(2). Give the namespace form its own name. Define type_ref_or_kind as today's type_ref plus Branch and Commit.

Tradeoffs and liability

Keeping the triple, freezing score_type's column, and refusing catalogue rank search each remove a second contract. That is the right cut. Pattern-scoped writes as later productions match the spike's count (24 of 60 forms need a new production, each at a token edition 3 refuses). Slots then buy a spelling and a diagnostic. They are not the source of compatibility.

The proof is the long-term liability. It is the gate for every later registry row that the rewriter must carry. A table that is shorter than the corpus, a T22 with two widths, and a formatter whose equality is unnamed each become a second break when the next RFC fills a slot and reuses the same proof. Five more revisions of this shape would grow a second grammar in the evaluation and a second equality in the spike. One table, one T22 sentence, and one named comparison are enough.

having as a Filter below Limit, with the overfetch ladder widening k when the result is short of limit, matches the ladder docs/dev/execution.md already runs for a short answer after later operators. That is one owner. Invariant 8 holds for a proof failure at boot (the graph quarantines). Invariant 9 holds for the F32 column. Invariant 12 holds while catalogue retrieve stays a slot. Invariant 13 fails while finding 1 stands: the corpus gate is the evidence, and the table does not reach it. No deny-list item is added. No Lance API is written.

Validation and limitations

  • Head reviewed: 667642b137d52af72642f50c46d46f5269a0fb11. State: open. Rechecked immediately before this comment. Pull-request reviews: none. Inline comments: none. Issue comments: the review on 6dc6315d, the review on 7c44b23d, and the spike report. No later comment reviews this head.
  • Related pull requests still open at the heads named above.
  • git diff --stat 667642b137d52af72642f50c46d46f5269a0fb11 HEAD -- crates/omnigraph-compiler/src/query crates/omnigraph-compiler/src/ir/mod.rs crates/omnigraph-compiler/src/schema/schema.pest is empty. The compiler bytes exercised below are the bytes at this head.
  • Run, on workspace main 3e0e467e: cargo test -p omnigraph-compiler --lib --offline -- test_traversal_direction_in test_traversal_direction_out test_undirected_traversal_rejected_on_asymmetric_edge. Result: 3 passed. test_traversal_direction_in accepts the incoming triple. test_undirected_traversal_rejected_on_asymmetric_edge expects T22 for $p <worksAt> $c.
  • Run: a pest 2.8.7 fragment of the sketch's call_args, view_args (!{ }), undirected_edge, arrow_traversal, type_ref, annotation_args, and list_item. rrf($a, $b, k: 60), $x: Name($id), $p: Person followed by ($id) on the next line, $p <knows | likes> $d, $p -worksAt-> $c, $c <-worksAt- $p, $a -knows- $b, crm::Person, @Type, and [date("2020-01-01")] parse. crm::@Operator, String?, Vector(2), @range(age, 0..200), and @card(0..1) do not. !{ } is a legal pest 2.8 modifier (non-atomic).
  • Code inspection at this head: score_type, score_column, RRF_COLUMN, query_file, type_ref in both grammars, cardinality, body_constraint, IROp::Expand, and the two GQT files named above.

GQT regression proof does not apply. This pull request changes documentation only. It changes no query result, error, or plan.

Limits: I did not re-run the spike, the competence instrument, scripts/check-docs.py, or the workspace test graph. I did not use a CI run on this commit as evidence. The pest fragment stubs expr. It checks the productions named above. It does not check the whole file.

ragnorc commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Recommendation: revise.

This review covers head 174fb239b96a9145c1c6ec223a382511a921a5e6. It reviews the change 667642b137d52af72642f50c46d46f5269a0fb11..174fb239b96a9145c1c6ec223a382511a921a5e6 only. The comment on 667642b1 stands for every finding this commit leaves open.

Why

A score has to stay on its binding after the ranked scan. return, having, and a traversal that runs from that binding read the same column. This commit writes that lifetime into the retrieve section.

What this commit changes

docs/rfcs/2026-10-28-gq-3-grammar.md gains one sentence (lines 342-344). Outside rrf, an arm column is a value of its binding for the rest of the read. return, having, and order may read it. A later traversal from that binding keeps the column. The rrf sentences that follow are unchanged. Under rrf the arm column is not a value (T33). The fusion carries the fused column. RFC 0048 carries each arm column through RankFuse.

How the sentence fits

The sentence is in "The retrieve clause in this release". Clause order stays match, then retrieve, then return, having, order, limit (lines 276 and 580). A traversal stays inside match. "Later" is plan order. Lowering roots the ranked binding, and the traversal runs after that scan.

Impact

This commit resolves none of the six findings from the review on 667642b1, in full or in part. Three blocking findings stay open. Three optional findings stay open. The new sentence matches the engine and the programme text in PR 1213. It adds no defect.

Earlier findings

Blocking findings that stay open:

  1. The fused score has no legal spelling, and the rewriter table omits live projections. This commit leaves the rrf exception in place.
  2. T22 still has two widths, in the freeze row, the refusals list, and the traversal section.
  3. The formatter swap still changes Expand. IROp still has no Eq. The proof text still says the trees are equal.

Optional findings that stay open:

  1. Evaluation §10.1 still describes a null outside the window. §9.5 says the row is absent.
  2. The generic annotation sentence still omits cardinality and body_constraint.
  3. The sketch's type_ref still shares its name with today's parameter type, and type_ref_or_kind still has no production.

Blocking

No new blocking defect.

Optional

No new optional note.

Tradeoffs and liability

The non-rrf column has one owner: the binding. Expand keeps the source columns (crates/omnigraph/src/engine/operators/expand.rs:287). The execution guide already requires a traversal to keep a score (docs/dev/execution.md:926). The grammar RFC now states the same rule. The commit adds no operator and no second store of the column.

The rrf column stays with RFC 0048. PR 1213 stage R is the stage that carries each arm column through RankFuse so an arm under rrf becomes a value (docs/rfcs/2026-10-31-gq-3-programme.md:157 at 2f230acc). Stage B keeps today's two F32 columns. Each case has one owner.

Five more sentences of this shape stay cheap when each one names that same column. The open liability is the rewriter table, the two widths of T22, and the unnamed formatter comparison. Those three become a second break when the next RFC reuses them. This sentence does not add a fourth.

A filter in match stays illegal for an arm (line 346). That keeps the column out of the clause that runs before the arm is bound (invariant 9). The arm name stays an alias over the scan column (invariant 6). Invariant 13 stays open while blocking finding 1 stands: the corpus gate is the evidence, and the rewriter table does not reach it. The commit adds no deny-list item. It writes no Lance API.

Checked against the code

This head contains main 3e0e467e. The merge-base is 3e0e467e. The engine files cited here match main.

  • ExpandExec output is the input schema, the destination id, then any bound-edge columns (expand.rs:287-294). A score column on the input stays.
  • A ranking is a root scan. Lowering roots the component at the ranked binding (docs/dev/execution.md:931-932). A ranked scan that a traversal reaches is a lowering defect (crates/omnigraph-planner/src/optimizer.rs:2781-2786). After rooting, the traversal runs from the binding. The passages example writes $r hasPassage $p and projects $p.lex (docs/rfcs/2026-10-28-gq-3-grammar.md:232-244). The written triple points at the arm. Rooting runs the physical traversal from $p. The new sentence covers that plan.
  • nearest_projected_through_traversal expects the F32 score beside the neighbour (crates/omnigraph-gqt/cases/issue_640_search_score_projection.gqt:197-216).
  • Lance 13.0.0's Rust scanner appends _distance for nearest search and _score for full text (lance-13.0.0 src/dataset/mem_wal/scanner/vector_search.rs, src/dataset/mem_wal/scanner/fts_search.rs). Lance does not traverse. Column survival belongs to Omnigraph. The Python vector quickstart prints a column named score. The engine reads the Rust names. This commit does not change that mapping.

PR 1213's stage text agrees with the rrf split. Its copy of the grammar file is still blob f54dbde7, the text before this commit. The programme sentences do not contradict the new sentence. That branch has not absorbed 174fb239.

Validation and limitations

  • Head reviewed: 174fb239b96a9145c1c6ec223a382511a921a5e6. State: open. Rechecked immediately before this comment. Pull-request reviews: none. Inline comments: none. Issue comments that review a head: 6dc6315d, 7c44b23d, 667642b1, and the spike report. No other comment reviews this head.
  • Local check, isolated checkout of this head: python3 scripts/check-docs.py with the pinned docs packages. Result: Documentation OK (445 Markdown files checked).
  • Exact-commit CI, run 41405719908, head 174fb239. Check AGENTS.md Links passed. That job runs scripts/check-docs.py and typos. The CI Gate job passed. GQT (ordinary) passed. The run conclusion is failure because Azurite Azure Integration failed. The job log was unavailable (HTTP 404). This commit edits one RFC paragraph. That job is not evidence about the sentence.
  • Code inspection at this head, at the lines above.
  • A GQT regression does not apply. This pull request changes documentation only. It changes no query result, error, or plan.

Limits: I did not run nearest_projected_through_traversal by itself. I did not re-run the spike or the competence instrument. I did not read the Azurite log.

@ragnorc

ragnorc commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Revised at 667642b1 (merged with main 3e0e467e; docs/rfcs/README.md conflict resolved). This reply maps the revision to the two reviews and the spike report.

Blocking findings from the first review

  1. Score column. Frozen as what score_type publishes: F32 non-null for nearest and bm25, F64 for rrf, a row outside the window absent. The arm is a projection alias over the column the ranked scan appends. The rewriter names its generated arms _distance and _score, so no result column changes name or type for a migrated query (the two unaliased GQT projections included).
  2. IR proof. Kept. An arm reference is resolved to its call before IR is built, so order { $d._distance } lowers to the same IRExpr::Nearest and search_node reads what it reads today. The spike's numbers are cited: 1,818 declarations and 2,968 generated reads IR-equal; 3 expect error codes move to T65 through the tool's code map, and the corpus gate counts query-text edits only.
  3. Direction. Both written orders of a directed triple stay legal, as in edition 2. queries fmt writes schema order and lint names it. T5 keeps today's meaning. The compatibility list names every traversal spelling as unchanged, and the T22 narrowing (per-member <a | b>) is recorded as the one additive semantic change.
  4. file rule. Today's query_file is extended by edition? and declaration*; show, branch, commit, load, explain, statement_trailer and missing_param_list stay.
  5. Annotations. @description and @instruction typed; @example and @if_commit slots (@if_commit must become the one source of the request precondition, writes RFC). annotation_args is defined with =, the .pg spelling.
  6. Versions. blocked_on now requires PR 805, 792 and 793 to remove the majors and refusals the decision log names (805's (3, 0)/(4, 0); 793's "adds no clause" and deferred refusals, which land here; 792's removals and T27 tightening, withdrawn because a registry row is never removed). PR 884 is recorded as a minor against a stale base. supersedes stays empty.

Optional items: the shared-expression quote, bump base (2, 3) / explain 7, boot behaviour on a proof refusal (quarantine, as a parse failure today; one warning per source per boot), the lint prefix (L added to the query catalogue rule), the "Operators" heading, the 0047 citation as written, updated, and the comp_op note are all applied.

Blocking findings from the second review

  1. Incoming triple. Resolved as above: legal, formatter-canonicalised, no swap refusal. arrow_traversal is in clause before traversal.
  2. Evaluation asset. §9.4, §9.5, §9.9, §11.1, §11.2, §11.6, §13.3 and §14.2 match the RFC; first-draft choices (arrows, contract, F64?, the swap fix, the HTTP bump, recall_achieved) are marked as such.
  3. Catalogue search. A retrieve over a catalogue type is a slot behind the catalogue RFC's refusal; the amendment that lifts it must name a mechanism that opens no Lance dataset. The "ordinary table" sentence is gone.

Spike amendments

A1 to A5 as above (having over a score = top limit rows of the passing population through the ladder, own type rule T67; 0048's three refusals land in edition 3). G1 to G8 applied in the grammar sketch, each named in the parser-test list. The sentence that every later capability is a row, node or slot is replaced by the measured finding (24 of 60 need a new production, each additive at a token edition 3 refuses) under "What a slot buys". Also: null leaves prop_match; a catalogue name takes no namespace segment; a mixed schema-and-query file is a slot; plan acceptance and RFC 0047's exact fragment are addressed in the retrieve section.

One choice to flag: the lexical rows stay rather than being removed in edition 3, because match_terms is the lexical RFC's and ships after the break. The Alternatives table records it.

@ragnorc

ragnorc commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Added "The syntax by example" (b48c9a5): one schema, then one worked example per form of edition 3 (file and schema, bindings and every filter operator, every traversal spelling, blocks, return and aggregates, retrieve with having and limit $k, writes with as commit and as unattached branch, every statement, the catalogue), the slots with their owners, the refused respellings with the fix each prints, and the queries fmt rewrites. The examples are the parser-test fixtures the Evidence section names.

ragnorc commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Recommendation: revise.

This review covers head b48c9a5a70e9f8a726a846ad026a1d960dbc4d37. The diff since 174fb239b96a9145c1c6ec223a382511a921a5e6 is one commit, b48c9a5a. 95328a21 and 667642b1 are parents of 174fb239. The merge of main 3e0e467e is 7e396a83, also a parent of 174fb239. The reply that maps findings to 667642b1 describes text the review of 174fb239 already covers.

Why

The new section is the text a later parser test will paste. Several of those pastes do not parse, or they do not typecheck, under the rules this RFC already states. The six findings from the review of 174fb239 stay open.

What this commit changes

docs/rfcs/2026-10-28-gq-3-grammar.md gains "The syntax by example". One schema, then one block per surface, then a table of refused respellings, then a table of queries fmt rewrites. One sentence in "One spelling per meaning" changes with it. A bare node in return is one object column. $a { slug } is a later production.

How the section is organized

The introduction splits forms into works, slot, and refused. The formatter table is a fourth class. Two of its rows wait on later RFCs (is, match_terms). The closing sentence of that table is the line the section draws between queries fmt and queries migrate.

Impact

The six earlier findings stay open. Six new defects are in the examples. They block the use of those examples as the parser-test fixtures the Evidence section names. The bare-node sentence matches the compiler. A large part of the traversal example typechecks once two lines are removed.

Earlier findings

Blocking findings that stay open:

  1. The fused score still has no single spelling. The hybrid example now writes return { rrf($d.vec, $d.lex, k: 60) as score } and order { rrf($d.vec, $d.lex, k: 60) }, and it types the value F64. The retrieve section still says a ranking call in order or return is T65. The refusals list repeats that. The rewriter table still has no row for a fused projection, a bm25 projection, a Boolean ranking projection, or a repeated ranking key. The example and T65 cannot both be the test.
  2. T22 still has two widths. The freeze row, the refusals list, and the traversal section still disagree. Evaluation §11.6 still says "today's semantics". The new traversal example uses the per-member rule. It does not make the three sentences one sentence.
  3. The formatter swap still changes Expand. IROp at 3e0e467e derives Debug and Clone only. The proof text still says the trees are Eq. The new formatter sentence adds a further contradiction. See the new finding below.

Optional findings that stay open:

  1. Evaluation §10.1 still says projectable per-arm scores that are null outside the window are §9.5's model. §9.5 says the row is absent. The header covers §9 and §11. It does not cover §10.1.
  2. The sketch still omits cardinality and body_constraint. The new schema uses @card(0..1) and @range(age, 0..150). Those parse today because schema.pest keeps the two rules. The sketch's annotation_value still has no range.
  3. type_ref_or_kind still has no production. The sketch's type_ref still rejects [String], I32?, and Vector(1536). The examples use all three, and they use Branch and Commit.

Blocking

  1. The schema example does not parse under the schema rules this RFC says move unchanged. @instruction stands after implements Named. The node rule is type name, then annotations, then implements, then the body (schema.pest, node_decl). The file fails at implements with expected annotation. The same declarations with @instruction before implements parse. edge Authored puts @key(@src, @dst) outside the body. That position fails with unexpected constraint_name. The accepted spelling is inside the braces (schema/parser_tests.rs, test_edge_key_body_group_is_the_only_accepted_spelling). A schema with those two edits parses and builds a catalog.
  2. Two worked comparisons mix Date and DateTime. Person.joined is Date and now() is DateTime, so $p.joined <= now() is T7 (cannot compare Date with DateTime). $p.joined >= date("2025-01-01") typechecks, because date() is Date (typecheck.rs, literal_type). Doc.published is DateTime and date("2026-01-01") is Date, so the exists block in by_blocks is the same T7. The same block with datetime("2026-01-01T00:00:00Z") typechecks. The RFC says these filters keep today's rules.
  3. $p *{1,2} $near does not match the traversal this RFC says is unchanged. $near has no node binding. A wildcard requires a declared type on both ends (typecheck.rs, the wildcard check, and the user-guide traversal page). The line is T5. $p: Person, $near: Person, $p *{1,2} $near typechecks. The comment "any edge type" is wider than that rule. The wildcard selects edges between the two declared types. A bound above one hop needs the same type at both ends. The paragraph under the example already says that. The example should do it. With the opposed <worksAt | employs> line removed and this wildcard left in, the query is still T5. The T22 on that opposed line is the edition-3 change, so that line is not this finding.
  4. The <-e- repair reverses the arrow. The table fixes $c <-worksAt- $p to $c worksAt $p. The traversal section says the repair preserves meaning, is computed without the schema, and cites spike Q008. The spike test the_triple_is_unchanged_and_arrows_print_the_triple fixes $f <-knows- $p to $p knows $f. That is the arrow's direction. On a same-type edge the direction is the only meaning. Left-to-right text order would print $f knows $p. Make the cell $p worksAt $c.
  5. The introduction says a form that is not in the examples is not in the grammar. The freeze list still contains forms this section does not show: $a (knows|likes){1,3} $b, $a * $b, @example, Vector(n, distance=...), and the catalogue edges @from and @implements. That sentence is a second inventory. Delete it. The freeze list remains the list of forms. The same block also puts @valid and @analyzed with the slot refusals. The schema-roles row says those two are accepted as SchemaIR features and refused at use. A stored declaration and a T6x refusal of the declaration are different outcomes. PR 1213, still at 2f230acc, puts @valid in stage W1 and @analyzed in stage L. That matches "refused at use". Move the two declarations out of the slot block.
  6. The formatter section says a formatter rewrite never changes the typed IR, and then says the rewriter's edits above do change it. The table above this sentence is the formatter table. The rewriter table is below. The rewriter section refuses a rewrite unless the typed IR is equal. Both sentences cannot stand. IROp at this head's compiler (3e0e467e, ir/mod.rs) derives Debug and Clone only. The incoming-triple row in the formatter table is the Expand pair derived Eq would reject. src_var and Direction both change. Name the comparison the spike ran, or compare result rows. Do not say the trees are Eq.

Optional

  1. The statements fence is not one query_file. Each of the seventeen lines parses alone, including branch create fix from commit "01J…". The seventeen lines in one file fail with a branch statement stands alone in its file. The paragraph under the fence already says a file holds one statement. Say that each line is its own fixture.
  2. update Person set { bio: null } has no production in the sketch. The parameters section says null is the whole value of a mutation assignment and is absent from a read. Today's parser reports the reserved-word error on this line. Show mutation_assignment so the fixture cannot admit null in a read. PR 1213 says null in assignment works from this RFC's stage.
  3. The write comment calls from and to reserved. The reserved set in this RFC is the six words and, or, not, is, null, and in. from and to are endpoint assignment names on an edge insert (typecheck.rs, the insert path). Call them endpoint names.

What holds

These checks passed on the current compiler. The compiler files named below are identical at 3e0e467e and at workspace main.

  • A corrected schema parses and builds a catalog. The correction is @instruction before implements, and @key(@src, @dst) inside the Authored body. @embed, @range, @card, an enum @index, and edge In then parse. An enum @index is a scalar BTREE, which matches the comment that an enum takes no full-text index (types.rs, index_kind).
  • graph_shapes typechecks after removal of $p *{1,2} $near and $p <worksAt | employs> $firm. The incoming triple, the quoted "in", the edge binding, knows{2,2}, and return { $p } all typecheck. A bare node is one struct column (typecheck.rs, projection_field). The new one-spelling sentence matches that.
  • by_blocks typechecks after the datetime(...) change. The edition-2 subset of colleagues typechecks, including order { friends desc }, count(distinct $f.age), and count($f) > 3 as busy.
  • query operators() over @Operator typechecks. The catalogue type has name and inputs.
  • Each statement line parses. String is a legal nearest query today (typecheck.rs, the nearest query check, and test_rrf_ordering_ok_with_string_nearest_limit). The hybrid comment that $vq may be a String matches that rule.

Tradeoffs and liability

Worked examples are the right addition. A reader can see one schema and one form. The Evidence section already asks for one parser test per freeze-list row. The examples can be those tests.

The cost is a third inventory. The freeze list, the grammar sketch, and this section can drift. This commit is the drift. The introduction makes the examples the grammar. Five more sections of this shape would leave the freeze list as the text nobody pastes, and the examples as the text the parser is held to. One list should own the forms. The examples should illustrate that list, and each example should parse under it.

The older liability is unchanged. The rewriter table, the two widths of T22, and the unnamed Expand comparison are still the proof later registry rows will reuse. The new sentence that the rewriter may change the typed IR makes that reuse worse. A later RFC can cite it and ship a second Expand.

The non-rrf arm column still has one owner, the binding. This commit does not add an operator or a second store of the column. It writes no Lance API. Invariant 9 still wants one typed spelling of the fused score. The hybrid example and T65 are two spellings. Invariant 13 still wants the corpus gate to meet the rewriter table. The table is still short of the corpus. The new fixtures do not repair that gate. They add fixtures the gate would reject.

Keeping the triple, and refusing catalogue rank search, remain the right cuts. They are not changed here.

PR 792, PR 793, PR 805, and PR 884 are still open at c5d33c2931a1, 5e6e82e102d0, 49f436ce618b, and 24b60eba9e37. The decision log's demands on them are unchanged. PR 1213 is still at 2f230acc. Its stage text agrees with "refused at use" for @valid and @analyzed. It does not agree with a T6x refusal of those declarations.

Validation and limitations

  • Head reviewed: b48c9a5a70e9f8a726a846ad026a1d960dbc4d37. State: open. Rechecked immediately before this comment. Pull-request reviews: none. Inline comments: none. Issue comments that review a head stop at the review of 174fb239 and the author's two replies. No other comment reviews this head.
  • Diff checked: git diff --stat 174fb239b96a9145c1c6ec223a382511a921a5e6 b48c9a5a70e9f8a726a846ad026a1d960dbc4d37 is one file, 383 insertions and 1 deletion. git merge-base --is-ancestor confirms 95328a21, 667642b1, 7e396a83, and 3e0e467e are ancestors of 174fb239.
  • Local probe, on workspace main, whose schema.pest, query.pest, typecheck.rs, ast.rs, types.rs, and ir/mod.rs match 3e0e467e (git diff --stat empty). A temporary test rfc1202_example_probe in crates/omnigraph-compiler/src/query/typecheck_tests.rs parsed and typechecked the examples. Command: cargo test -p omnigraph-compiler --lib --offline -- rfc1202_example_probe -- --exact. Result: 1 passed. The outcomes are the ones in "Blocking" and "What holds". The test was removed with git checkout -- that file. The worktree is clean. Nothing was pushed.
  • Exact-commit CI, run 41523560455, head b48c9a5a. Check AGENTS.md Links passed. That job runs scripts/check-docs.py and typos. GQT (ordinary) passed. The CI Gate failed because Storage Upgrade Compatibility failed in "Run required storage upgrade crossversion tests". The job log was unavailable. This commit edits one RFC section. That job is not evidence about the examples.
  • Code inspection at the lines above, and the spike test file crates/omnigraph-compiler/src/gq3/tests.rs at spike/gq3-1202 for the arrow repair. The programme text was read at PR 1213 head 2f230acc.

This pull request changes documentation only. It changes no query result, error, or plan. A GQT regression does not apply. The probe is the check that the worked examples match today's grammar where the RFC says the form is unchanged, and match the RFC's own rules where the form is new.

Limits: I did not re-run scripts/check-docs.py locally. I did not re-read the Lance format pages. The new sentences do not add a Lance call. The String nearest query and the enum index kind were checked in the compiler. I did not re-run the spike or the competence instrument. The probe uses today's parser, so retrieve, having, limit $k, parameter defaults, and mutation fail to parse here by design. Those forms were checked against the RFC text, not against an edition-3 parser.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant