Repository navigation
compiler: the diagnostics contract for refused queries (RFC 0047, PR 1) - #759
Merged
Merged
Conversation
ragnorc
force-pushed
the
rfc-0047-pr1-diagnostics
branch
2 times, most recently
from
September 25, 2026 08:59
06a19c8 to
11bf2a9
Compare
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.
ragnorc
force-pushed
the
rfc-0047-pr1-diagnostics
branch
from
September 28, 2026 14:28
11bf2a9 to
391b45d
Compare
# Conflicts: # crates/omnigraph-compiler/src/query/typecheck.rs
Two paths still dropped a refusal's diagnostic. The engine's mutation door turned a parse failure into a plain bad request, so a Rust caller of `mutate` got no code and no position; it now shares the read door's conversion (`query_lookup_error`), so both doors keep the diagnostic. The human output of `cluster import`, `cluster validate`, `cluster plan` and `queries validate` printed only the message, which since the diagnostics contract no longer carries the parser's position. They now print the same location and fix lines as the error lane, from one renderer (`diagnostic_detail_lines`). Found by a Codex review pass.
azimafroozeh
approved these changes
Sep 30, 2026
azimafroozeh
left a comment
Collaborator
There was a problem hiding this comment.
Added my review as a follow-up commit:
- Typed suggestions with UTF-8 byte edits and applicability, so agents can apply fixes without parsing prose.
- Grammar validation before emitting machine-applicable Q002 repairs.
- Preserved structured diagnostics through stored-query loading and validation.
- Added Q005 source positions and corrected error-code descriptions.
azimafroozeh
enabled auto-merge
September 30, 2026 12:02
ragnorc
added a commit
that referenced
this pull request
Sep 30, 2026
3 tasks done
aaltshuler
pushed a commit
to aaltshuler/omnigraph
that referenced
this pull request
Sep 30, 2026
* rfc: RFC 0047, search plan truth on the planned engine * rfc: link RFC 0047 to its discussion and sibling PRs * rfc: RFC 0047 on engine v2 as the only engine * rfc: RFC 0047 step 1 shipped in PR ModernRelay#759 * rfc: RFC 0047 registry row in progress * review * hm --------- Co-authored-by: azim afroozeh <13484327+azimafroozeh@users.noreply.github.com>
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 an in-context competence measurement: 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 rollout step 1 of RFC 0047, "Search plan truth" (#791). 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
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 theT27refusal in rollout step 3 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— okRebase note (2026-09-25)
mainmoved by five pull requests (#760, #768 to #772) after this was opened: planned v2 read execution, anexplainstatement, unified expressions and the new type codesT38toT46. The branch was rebased by re-deriving the change on the new files rather than by resolving hunks: the type checker's 151 error sites were converted again by the same mechanical rule, the run-time chosen code of the block-keyword refusal (T9orT39) and the search-predicate constant (T38) became code values, and this pull request's own new codes moved toT47toT53. Two of main's new parser tests pinned theParsevariant for a refusedexplainfile; they now check the parse diagnostic. Every owner listed under local verification was run again on the rebased head: compiler 417, GQT corpus 190 cases, CLI 90/24/23, server 147/77/104/18, Clippy and formatting clean, docs checks green.Rebase note (2026-09-28)
mainmoved by eight pull requests (#754, #776, #697, #777, #778, #779, #780, #781). The one conflict came from #779, which movedOmniErrorinto the newomnigraph-corecrate and leftcrates/omnigraph/src/error.rsas a re-export.OmniError::diagnostic()moved with it tocrates/omnigraph-core/src/error.rs; nothing else changed. Checked on the rebased head: the workspace builds with all targets; the canonical workspace test graph passes; the GQT corpus passes (191 of 197 in the full run, and the other six, all simulation cases that hit the 10-second case limit on a heavily loaded machine, pass when run alone); workspace Clippy with-D warnings, formatting, docs, AGENTS.md andtyposchecks are clean.Merge note (2026-09-29)
mainmoved again (#795, #798, #799, #802, #803), and the repository now refuses force-pushes, so this branch takesmainas a merge commit instead of a rebase. The only conflict was in the type checker, where #803 added list membership (in): its two new refusals and the empty-list case now use the typed diagnostic like every otherTsite (CompilerError::typed(T7, …)), so their text is unchanged. Checked on the merged head: the compiler tests (417), the full GQT corpus (224 cases, #803's membership cases included), workspace Clippy with-D warnings, formatting and the docs checks all pass.Review pass (2026-09-30)
A Codex review (GPT-6-Astra, high effort) of the whole diff against
mainfound two places that still dropped a diagnostic, both fixed inde33129a:lower_named_mutationturned a parse failure into a plain bad request, so a Rust caller ofmutategot neither the code nor the position; the CLI and the server were not affected because they parse first. Both doors now share one conversion,query_lookup_error. New test:writes.rsrefused_mutation_text_keeps_its_parse_diagnostic(codeQ001, line 1 column 31, graph head unchanged); it failed before the fix.cluster import,cluster validate,cluster planandqueries validateprinted only the message, which no longer carries the parser's position. They now print the same--> line, columnandfix:lines as the error output, from one renderer,diagnostic_detail_lines. Test:cli_queries.rscluster_import_rejects_a_broken_query_naming_it_and_wheregains a query that does not parse and asserts--> line 1, column 46; it fails when the renderer change is reverted.A second Codex pass over the fix commit found no regressions. Checked locally: engine
writes47/47, CLIcli_queries24/24 andcli_cluster63/63, Clippy on both crates with-D warnings, formatting and docs checks.Notes for reviewers
T…catalogue gives every code a one-lineshort. Per-codefixtext is written by the pull request that owns each code (T27in rollout step 3, the planner's refusals in step 2) 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.