Uh oh!
There was an error while loading. Please reload this page.
test(scripts): gate the filter slot's wire-alias spellings against the normalizer's table (#8002) - #8182
Merged
Conversation
…e normalizer's table (#8002) The ONE filter slot has four wire spellings declared in TWO packages: `where`/`filter` come from the spec's `RPC_QUERY_ALIAS_SLOTS`, while the wire-only `filters`/`$filter` are declared in `metadata-protocol` (`WIRE_QUERY_ALIAS_SLOTS`) and named LITERALLY in `packages/rest`, which gates the slot's arity at the querystring ingress (#7390). Nothing reconciled them. A fifth wire-only spelling added to the normalizer's table would fold correctly and be silently ungated at the ingress, putting repetition on that spelling back on the misdiagnosis #7390 exists to remove. `#7390 §5 filterSlotSpellingsAreComplete` pins the REST side alone; the half holding the truth lives in a package that is only a devDependency of the one that gates the ingress. A runtime import cannot join the two sides — `WIRE_QUERY_ALIAS_SLOTS` is a module-private const, and `@objectstack/metadata-protocol` is a devDependency of `@objectstack/rest` — so the gate reads source through the TypeScript compiler API, the idiom a dozen sibling gates already use for cross-package source truth. Every anchor is asserted: a declaration that moved, an initializer whose shape changed, or an array element that is neither a literal spelling nor the recognized spec-derived spread is a loud failure, never an empty corpus reported as a pass (#4690). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WocN37om5bw81JDoEEMA2e
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
os-zhuang
marked this pull request as ready for review
August 12, 2026 19:02
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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.
Fixes#8002
Adds
check:filter-alias-parity— a consistency gate asserting that the filter slot's wire spelling set is identical on its two sides.The seam
The ONE filter slot has four wire spellings, declared in two packages with different ownership:
where/filtercome from the spec'sRPC_QUERY_ALIAS_SLOTS(packages/spec/src/data/data-engine.zod.ts).filters/$filterare wire-only — no schema declares them.packages/metadata-protocolextends the spec table with them locally (WIRE_QUERY_ALIAS_SLOTS), which is what makes them fold intowhere.#7390 added a third reader:
packages/restgates the slot's arity at the querystring ingress (FILTER_SLOT_QUERY_PARAMS,packages/rest/src/query-multiplicity.ts). It deriveswhere/filterfrom the spec table and namesfilters/$filterliterally, because it cannot derive them.Nothing reconciled the two literal halves. A fifth wire-only spelling added to the normalizer's table folds correctly and is silently ungated at the ingress: repetition on it falls back to the misdiagnosis #7390 exists to remove — a 400 naming a malformed filter, when every filter the caller sent was fine and the mistake was sending two.
The opposite direction was already covered.
#7390 §5 filterSlotSpellingsAreCompletepinsFILTER_SLOT_QUERY_PARAMSto exactly the four, so a spelling added on the REST side alone goes red there. This gate deliberately does not duplicate that pin; it covers the half that test cannot reach.Why a source scan, and why the AST
A runtime import cannot join the two sides, and both reasons were measured before the script was written:
WIRE_QUERY_ALIAS_SLOTSis a module-privateconst— not exported, so there is nothing to import even from inside the package.@objectstack/metadata-protocolis a devDependency of@objectstack/rest(workspace:*), so no runtime edge exists between the two packages that could carry the table.That leaves reading source, and the repo already has a house answer for cross-package source truth: the TypeScript compiler API, used by a dozen sibling gates (
check:route-envelope,check:meta-type-normalized,check:kernel-hook-pairs, and others). Regexes were rejected for the reason that would have rotted this gate first — all three declarations are documented in prose that quotes the spellings, and two of the three are IIFEs whose shape a pattern would have to re-learn on every refactor.The rot rule: an unreadable shape is RED, never green. The failure mode of any source-reading gate is the silent one — a refactor moves the declaration, the reader matches nothing, two empty sets compare equal, and the gate reports a pass over a corpus of zero. Every reader asserts its anchor: a missing declaration, an initializer whose shape is not the one the script knows, an array element that is neither a literal spelling nor the recognized spec-derived spread — each is a loud failure naming the file and what it expected ("a scan that reads nothing cannot report a pass", #4690).
Two hops, not one
WIRE_DOLLAR_ALIASESfolds$xinto its bare spelling before the slot table folds, so a$-alias whose bare spelling is a filter spelling is itself a filter spelling — one the ingress would have to know about. The normalizer side therefore reads both tables, and the self-test pins that shape. Today that set contributes nothing ($filteris declared on the slot table, deliberately, and no bare filter spelling appears in the pair table), which is exactly why it is worth pinning.Proof it can fail
Measured on the real tree, not asserted (planted, run, reverted with
git checkout --):mainas it stands4 wire spelling(s) of the "where" slot, identical on both sidesWIRE_QUERY_ALIAS_SLOTSonlyFOLDED BUT UNGATED: where_clause5 wire spelling(s) ... identical on both sidesFILTER_SLOT_QUERY_PARAMSonlyGATED BUT NOT FOLDED: where_clauseThe
--self-testcovers the same four cases over planted fixtures, plus the two-hop$alias and four rot shapes (a missing ingress declaration, a normalizer that stops deriving from the spec table, a non-literal ingress element, an empty spec table), plus its own CI wiring. It runs before every gate invocation, per the family convention.The failure message
It names both sides with their files, prints the symmetric difference in the direction that happened, and says what to do:
Scope
This is #8002's option 2. Option 1 — hoisting the wire-only vocabulary onto the
@objectstack/specexport surface — is out of scope by triage and is not foreclosed: whether the wire vocabulary belongs in the spec at all is a spec-seat question, and these spellings exist precisely because no schema declares them. The gate is the reversible move: no public API, no dependency-graph change. If that hoist ever lands, both sides become derived, the sets stay equal by construction, and this script can be deleted rather than migrated. The script's header says so.Changes
scripts/check-filter-alias-parity.mjs— new gate.package.json—check:filter-alias-parity, in the family's--self-test && runform..github/workflows/lint.yml— one step in the ESLint job, beside the other source-shape guards.No published package changes (the root manifest is
private), so this carries theskip-changesetlabel rather than a changeset.Verification
pnpm check:filter-alias-parity— self-test green, gate green.node scripts/pm/dispatch-gates.mjs; all green:check:nul-bytes,check:required-contexts,check:workflow-status-functions,check:node-version,check:shard-attestation,check:changeset-gate-self-tests,check:type-check-coverage.npx eslint scripts/check-filter-alias-parity.mjs— clean.Generated by Claude Code