feat(pxq): strict-mode AST validator - #40
Merged
Merged
Conversation
Closes the last item from the original pxq design doc. When a top-level message type is bound to the document — via -m, an @type directive, or one of the four @proto shapes — the parsed gojq query AST is walked before compile, and any direct field-chain access that doesn't resolve against the schema is rejected at compile time with a did-you-mean hint. Three new CLI flags: -m, --message fully-qualified message name binding the document root; required for --strict when no @type is present --strict force strict mode; errors when no root type is bound --loose force loose mode (skip validator) even when bound The validator (strict.go) walks gojq.Query → Term → Suffix recursively, tracking the current message descriptor down field chains. Field lookups go through MessageDescriptor.Fields().ByName; missing fields surface with a Levenshtein-distance-2 typo hint and the parent message's fully-qualified name. Dynamic access patterns (array indexing, pxf_directive(...), function calls, object construction) pass without validation — Stage E's contract is "no false positives, some false negatives": a passing query is guaranteed not to type-error on the field names the validator can see; dynamic patterns fall back to gojq's runtime behaviour. effectiveMode/resolveRootType encode the README's resolution rules: - auto-mode: bound → strict, unbound → loose - --strict + unbound → actionable error pointing at -m / @type / pxq infer-schema - --loose: always loose, regardless of binding - --strict / --loose mutually exclusive (caught in pickMode) Test coverage (20 new, 106 total): * effectiveMode across the {auto,strict,loose} × {bound,unbound} matrix * validator: known/unknown direct fields, did-you-mean radius cutoff, nested chains (both halves), chain-past-scalar permissiveness, pipe-both-halves-validated, non-path Terms (literals, object/array ctor, function calls) permissive, nil-root no-op, array-index-then- field permissive * resolveRootType: --message wins over @type, @type fallback, no-binding returns nil, no-schema returns nil * end-to-end via runQuery: strict rejects typo at compile time, loose runs typo to null per jq CLI smoke tests verify: * @type-driven strict mode kicks in implicitly * --loose disables validation * --strict without binding shows the prescribed help * Nested submessages validate transitively (.inner.s passes, .inner.t errors with the parent message name) Remaining design-doc items: comparison type-checking (.age > "30" where age is int32). Stage E intentionally scopes to field-name correctness; type-aware comparisons would need a type inferer covering arithmetic and operator-overload semantics — out of scope here.
3 tasks
This was referenced Jul 16, 2026
trendvidia
added a commit
that referenced
this pull request
Jul 24, 2026
- Implementation boxes #40–#43 ticked: protocheck v2.0.0 (2026-07-23) shipped the engine SPI (PR#22), function registration + init-time verification (PR#23), and validation execution — WithFailFast (PR#21), wire-aligned EnrichedViolation model (PR#24), carrier-rule dispatch with source-map enrichment (PR#26); catalog + i18n (PR#28, M6). v2.1.0 added per-element repeated/map dispatch (PR#35). - #50–#52 ticked: protolsp shipped RFC-001 extended-grammar parsing and semantic tokens (protolsp#218 Phases A/B, 2026-07-16), source-map navigation with descriptor fallback (protolsp#219 Phase C, #220), and annotation-aware diagnostics (#220; completion parity #228, runtime-violation overlay #231). - #70 ticked: protowire-go v1.3.1 (2026-07-23) wires the Validator seam through the pxf/pb/sbe decoders (protowire-go#49/PR#59) with the protocheck-side adapter (protocheck PR#38); protovalidate adapter as nested module (PR#60), noted on #17. - M0 spec boxes #4–#7 ticked (landed via GH #54; annotations.proto round-trip verified per GH #58); #6 acceptance boxes ticked against the shipped STABILITY.md section. - #19 annotated: §5.3 executable fixtures shipped (GH #135, PR #138); corpus expansion continues in GH #68. Remaining implementation fronts: #60/#61 (protobuf-go codegen) and #80 (OpenAPI, GH #93).
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.
Summary
Closes the last item from the original pxq design doc. When a
top-level message type is bound to the document — via
-m, an@typedirective, or one of the four
@protoshapes — the parsed gojq queryAST is walked before compile, and any direct field-chain access that
doesn't resolve against the schema is rejected with a did-you-mean
hint.
Demo
New CLI flags
-m, --message <FQN>--strictwhen no@typeis present--strict--loose--strictand--looseare mutually exclusive.What the validator catches (and what it doesn't)
The contract is "no false positives, some false negatives": a query
that passes validation is guaranteed not to type-error on the field
names the validator can see; dynamic access patterns fall back to
gojq's runtime behaviour.
Validated:
.foo,.foo.bar, …)|pipeOuter.inner.s)Not validated (passes through to runtime):
.tags[0],.tags[])pxf_directive(...),length, …){a: .x},[.x, .y]).age > "30"where age is int32) — outof scope; would need a full type inferer
Resolution rules
effectiveMode×resolveRootTypeencode the README's behavior:--strict+ unbound → actionable error pointing at-m/@type/pxq infer-schema--loose: always loose, regardless of binding--messagewins over@type;@typefalls back to nothingTest plan
go build ./...cleango test ./cmd/pxq— 106 tests pass (86 from prior + 20 new):effectiveMode× {auto, strict, loose} × {bound, unbound}cutoff, nested chains, chain-past-scalar permissiveness, pipe
halves, non-path Terms permissive, nil-root no-op,
array-index-then-field permissive
resolveRootType: --message-wins, @type fallback, no-bindingnil, no-schema nil
runQuery: strict rejects at compile time,loose runs typo to null
three new flags
Future work (not in scope)
.age > "30"where age is int32)pxf_directivefor transitive validation across more access patterns
This wraps the original pxq design doc — every item the README v1
described now exists in code.