Conversation
Every query compile refusal now carries a stable code, where the failure is, what was expected, and one fix, as one typed value from the parser to every surface that reports it. `QueryDiagnostic` lives in the compiler with a code catalogue; `CompilerError::Query` replaces `CompilerError::Type(String)` with a `Display` that keeps the legacy one-line form, so every existing assertion and `.gqt` needle holds. The parser positions a grammar mismatch at pest's deepest failure with the rules it expected (`Q001`). A declaration without its parameter list is refused at the name's end with the fix `query name()` (`Q002`) by a committal grammar recognizer, because pest tracks attempts per rule and the missing `(` is never an attempt it can name; settings and branch statement refusals carry their positions (`Q003`, `Q004`). The 124 coded typecheck sites convert mechanically to `CompilerError::typed`; eight uncoded user-facing refusals gain codes, and the descriptor's internal consistency errors become plan errors. On the wire, `ErrorOutput` gains an additive, skip-serialized `diagnostic` detail, so every existing error body is byte-identical. The server maps every compiler refusal through it at every door. The CLI treats `--format jsonl` as a machine format, prints the same error body for the embedded lane as for the served one, renders the four fields on stderr for human formats, and installs color-eyre without its environment and location footers. `queries validate --json` and `cluster plan --json` carry the same detail beside their messages, so a stored query a later release would refuse is a pre-upgrade finding.
A diagnostic's position carries a character column, the coordinate of the diagnostics contract; the guard's OpenAPI inventory now records the three occurrences (the schema prose, the property, the required name) as retained renderer vocabulary, not a tabular or physical column.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Background
OmniGraph runs queries written in GQ, its query language. When the compiler refuses a query, the caller gets one line of text. Today that line is all there is. The parser's message points at the first character of the file and names a grammar rule, and the CLI wraps it in colour codes and a backtrace hint. A type error carries a code such as
T33only as the first word of its message. Nothing tells the caller where the problem is or what to write instead.The callers that matter most are agents. An agent treats an error as the documentation it acts on, and it retries by default. We measured this with the in-context competence instrument (RFC 0047): 45 of 45 tasks ended correct, but 8 of 45 first attempts were refused, and every one of those refusals was the same case. The agent wrote
query name {without the empty parameter list, and the parser answeredparse error --> 1:1 … expected query_file. That message names neither the missing(nor the fix.What this change does
This is the first of nine pull requests that implement RFC 0047, "Search plan truth" (accepted on 2026-09-19 in #606). It lands the RFC's diagnostics contract. Every refusal of a query now carries four things:
Q…codes come from the parser,T…codes from the type checker.The measured case now reads:
The four fields travel as data through every surface that reports a refused query: the HTTP error body, the CLI in its human and machine formats,
omnigraph lint,queries validateandcluster plan.What does not change
Displayform is the legacy form (parse error: …,type error: T33: …), so every existing assertion and every.gqterror needle still holds. One exception: a parse refusal's one-line text no longer embeds the parser's multi-line rendering.diagnosticfield is optional and is omitted when absent..pg) parse errors are outside the contract and are untouched.Backing issue / RFC
implementationfield moves toin-progressthere after both land.How it is built
One type, converted once at each boundary.
QueryDiagnostic { kind, code, message, position, stage, fix }lives incrates/omnigraph-compiler/src/query/diagnostic.rs, with the code catalogue inquery/codes.rs.CompilerError::Query(Box<QueryDiagnostic>)replacesCompilerError::Type(String).CompilerError::Parse(String)stays for the schema parser.Q001, positioned at the parser's deepest failure and naming the rules it expected there. The missing parameter list isQ002, from a new grammar rule (missing_param_list). The pest parser records attempts per rule, not per token, so the missing(is never an attempt it can name. The rule matches a declaration whose name is not followed by(and reports it at the name's end. Settings refusals areQ003, branch and show statement refusals areQ004, and a declaration body the hand-written parser refused isQ005.T…message convert mechanically toCompilerError::typed(T33, …), with the code as a value instead of a string prefix. Eight refusals that had no code getT38toT44(one duplicate reusesT6). Six internal consistency checks in the result descriptor becomePlanerrors, because they are not user diagnostics.ErrorOutputgainsdiagnostic: DiagnosticOutput { code, position, stage, expression, expected, fix }. The RFC names flat fields. They are nested under one detail because a code needs a home the closedErrorCodeenum cannot give, and one optional field keeps every existing body byte-identical.ApiError::from_compilermaps every compiler refusal, on theCompilerarm and inclassify, so every door carries the detail.--format jsonlcounts as a machine format. The embedded lane prints the same error body as the served lane. The human lane printserror[CODE]: …, the position or stage, and the fix on stderr. color-eyre installs without its environment and location footers, so no backtrace hint appears.queries validate --jsonandcluster plan --jsoncarry the same object beside their messages. A stored query that a later release refuses is therefore a finding before the upgrade, which the deployment gate in PR 2 and PR 5 relies on.Resultthreshold: the code is a pointer-sized handle to a static catalogue entry, the byte offset is 32-bit, and a size test pins it.How to review
crates/omnigraph-compiler/src/query/diagnostic.rsandquery/codes.rsfirst. They define the whole contract.query/query.pestandpest_error_to_diagnosticinquery/parser.rs. The grammar comment explains why a recognizer rule is needed.CompilerError::Type(format!("Tnn: …"))becameCompilerError::typed(Tnn, format!("…")). Spot-check a few sites.docs/user/queries/diagnostics.md. It is the user-facing statement of the contract.Checklist
Q001,Q002andQ003positions and fixes), type checker (aT…error exposes its diagnostic), server unit test andopenapipins,data_routes(every door reportsQ001andQ002with position and fix), CLI (cli_queries: both transports, json, jsonl and human)docs/user/queries/diagnostics.md, cross-references in the queries guide, CLI reference, troubleshooting page, and the release noteLocal verification
cargo test -p omnigraph-compiler -p omnigraph-api-types -p omnigraph-cluster --lib— 382, 4 and 151 passedOMNIGRAPH_UPDATE_OPENAPI=1 cargo test -p omnigraph-server --test openapi openapi_spec_is_up_to_datethencargo test -p omnigraph-server --test openapi— 104 passedcargo test -p omnigraph-server --lib a_refused_query— passedcargo test -p omnigraph-server --test data_routes parse_error_precedes_policy_denial_on_every_door— passedcargo test -p omnigraph-cli --test cli_queries q002— passed (embedded and served)cargo test -p omnigraph-gqt --locked— complete corpus, 101 cases passed (everyexpect error: T…needle holds)cargo test -p omnigraph-cli --test cli_queries --test cli_data --test parity_matrix— 89, 24 and 23 passedcargo test -p omnigraph-server --test data_routes --test stored_queries --test openapi— 147, 74 and 104 passedcargo clippyon the six changed crates,--all-targets -- -D warnings -W clippy::dbg_macro— cleancargo fmt --all --check— cleancargo test -p omnigraph-vocabulary-guard --lib openapi_inventory_matches— passed after the threePositionOutputcolumnoccurrences were classified in the guard's inventory (the first CI run's only failure). The guard's git-based tests need a signing-capable environment locally and pass in CI.python3 scripts/check-docs.py,bash scripts/check-agents-md.sh,typos— okNotes for reviewers
T…catalogue gives every code a one-lineshort. Per-codefixtext is written by the pull request that owns each code (T27in PR 2,T26in PR 5) and by a follow-up sweep. This pull request is the carrier, not a rewrite of 130 messages.Q005is deliberately coarse. The hand-written declaration parser has 59 refusal sites without positions. Positioning them is the same follow-up sweep.docs/user/cli/reference.mdsits exactly at the 350-line cap of the docs check, which is why the diagnostics text lives on its own page.