Skip to content

refactor: single-source wire grammar for the simple filter dialect - #798

Merged
tada5hi merged 2 commits into
masterfrom
wire-grammar-797
Jul 20, 2026
Merged

refactor: single-source wire grammar for the simple filter dialect#798
tada5hi merged 2 commits into
masterfrom
wire-grammar-797

Conversation

@tada5hi

@tada5hi tada5hi commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Implements #797.

Summary

The simple dialect's filter wire grammar (filter[age]=>=18, filter[name]=~oh~, filter[id]=!1,2) was spelled three times by hand — the decode if-cascade in parser-simple's value.ts, the 15-case encode switch in codec-url's simple FiltersVisitor, and the template-literal input union in types.ts. This PR collapses all three into one declarative table in a new parser-simple/src/parameter/filters/wire/ module.

The table + complement derivation

FILTER_WIRE_SPEC holds 8 rows, one per positive marker operator (declared order = decode precedence). Negation twins are not rows: they derive from the complementOf relation in core's FILTER_OPERATOR_SEMANTICS (#792), so the wire table repeats nothing and the frozen '!<5'lt 5 quirk falls out by construction (the ordering family has no complements). in/nin stay a value shape (comma list through the eq spelling), not a marker.

Per-caller contracts

  • decodeFilterWireValue(input) returns a verdict ({ success: true, condition } | { success: false, code }) — the parser applies the schema drop-vs-throw policy and nothing else. The empty-value post-check that previously lived inside SimpleFiltersParser ('', '!', '<=', '~~' decode to empty conditions) is absorbed as the valueEmpty verdict — the last piece of wire knowledge leaves the parser.
  • encodeFilterWireValue(condition) throws typed AdapterErrors (subset law is unconditional) and self-verifies every emitted token by re-decoding it through the same table. The grammar has no escaping, so collisions are value-content-induced (eq 'foo~' would re-decode as startsWith 'foo') — per the RFC, the round-trip check survives inside the codec instead of dying, but it disappears from codec-url and from the public API.

Derived typed input (fixes a live drift bug)

The parseTyped input union is now computed from the table's literal prefix/suffix types plus the complement relation. The hand-written union had drifted: it only contained prefix forms, so the runtime-accepted `${V}~` (startsWith), `~${V}~` (contains) and their ! forms were type errors. The derived union is exactly legacy + those 4 members — a compile-time-only widening. Compile-time Equals pins (wire/checks.ts, checked by build:types, not exported from the barrel) freeze the derived types against table or core-semantics changes.

Export changes (pre-GA)

Symbol Fate
parseFilterScalar unchanged, re-homed to wire/scalar.ts — dialect-shared scalar vocabulary (parser-expression + expression encoder untouched)
parseFilterValue, serializeFilterValue internalized
parseFilterWireValue replaced by decodeFilterWireValue
URLFilterOperator enum deleted — marker chars now exist once, in the table
new FILTER_WIRE_SPEC, FILTER_WIRE_NEGATION, decodeFilterWireValue, encodeFilterWireValue, FilterWireDecodeFailure + wire types

codec-url's simple encoder shrinks by ~130 lines and keeps only the structural subset law (flat root-AND, one condition per field); its imports drop to encodeFilterWireValue.

Acceptance gate (wire frozen byte-for-byte)

  • Every pre-existing spec passes with zero assertion changes: parser-simple 126, codec-url 234, full nx run-many -t test green across all 8 test projects (uncached).
  • New boundary spec filters-wire.spec.ts (65 cases): the grammar's first direct token↔condition coverage, pinning the frozen quirks ('!<5'lt 5, '~'contains '~', '~5~'contains '5' raw, ','in [], markers inert inside lists) plus two agreement probes — every row decodes back to its own operator (order = precedence, no shadowing) and every wire-expressible complement twin spells as '!' + its positive form, matched against FILTER_OPERATOR_SEMANTICS.
  • Type-check cost of the derivation on parser-simple (tsc --extendedDiagnostics): Types 4180 → 4554, Instantiations 6780 → 7590, check time 0.09s → 0.11s.

Out of scope (flagged in #797)

A real boolean through parseTyped throws in value normalization and the condition is dropped, although the input type advertises booleans — pre-existing, wire-frozen; separate issue to follow.

tada5hi added 2 commits July 20, 2026 12:55
The marker grammar lived in three hand-written spellings: the decode
if-cascade in value.ts, codec-url's 15-case encode switch and the
template-literal input union in types.ts — which had already drifted
(the startsWith/contains suffix spellings the decoder accepts were
missing, so parseTyped rejected valid wire input).

One declarative table now drives all three: FILTER_WIRE_SPEC holds a
row per positive marker operator; negation twins derive from core's
complementOf relation in FILTER_OPERATOR_SEMANTICS (the '!<5' -> lt 5
quirk falls out by construction); the typed-input union is computed
from the table's literal types and pinned by compile-time checks.

decodeFilterWireValue returns a verdict (the parser only applies the
schema drop-vs-throw policy; the empty-value post-check moves into
the codec), encodeFilterWireValue throws typed AdapterErrors and
self-verifies every token by re-decoding it (the grammar has no
escaping, so collisions like eq 'foo~' are value-content-induced).

parseFilterScalar survives unchanged as the dialect-shared scalar
vocabulary; parseFilterValue/serializeFilterValue become internal;
parseFilterWireValue is replaced; the URLFilterOperator enum is
deleted. The wire format itself is frozen byte-for-byte — all
existing specs pass unchanged; the new filters-wire spec pins the
grammar directly at the token boundary, including the degenerate
quirks.

Refs #797
The simple encoder's 15-case operator switch, serializeLikeText and
verifyWireValue re-spelled the wire grammar and defined encoder
correctness by re-parsing through another package's decoder. The
visitor now delegates per-condition token spelling to parser-simple's
encodeFilterWireValue and keeps only the structural subset law
(flat root-AND, one condition per field) — marker characters, raw
LIKE text and collision policy are no longer expressible here.

Refs #797
Copilot AI review requested due to automatic review settings July 20, 2026 10:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@tada5hi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5879a349-5530-4445-954d-aa16d24aa738

📥 Commits

Reviewing files that changed from the base of the PR and between 4324130 and fd19f2b.

📒 Files selected for processing (13)
  • packages/codec-url/src/simple/encoder/visitors/filters.ts
  • packages/parser-simple/src/parameter/filters/constants.ts
  • packages/parser-simple/src/parameter/filters/index.ts
  • packages/parser-simple/src/parameter/filters/module.ts
  • packages/parser-simple/src/parameter/filters/types.ts
  • packages/parser-simple/src/parameter/filters/value.ts
  • packages/parser-simple/src/parameter/filters/wire/checks.ts
  • packages/parser-simple/src/parameter/filters/wire/constants.ts
  • packages/parser-simple/src/parameter/filters/wire/index.ts
  • packages/parser-simple/src/parameter/filters/wire/module.ts
  • packages/parser-simple/src/parameter/filters/wire/scalar.ts
  • packages/parser-simple/src/parameter/filters/wire/types.ts
  • packages/parser-simple/test/unit/parser/filters-wire.spec.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wire-grammar-797

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tada5hi
tada5hi merged commit 3d0607c into master Jul 20, 2026
7 checks passed
@tada5hi
tada5hi deleted the wire-grammar-797 branch July 27, 2026 07:53
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.

2 participants