Repository navigation
finding: the wire-only filter alias spellings (filters / $filter) are now declared in two packages, with no gate reconciling them #8002
Description
Activity
Triage: stays
finding, deliberately left withoutdomain:*— the lane is decided by the chosen shape (option 1 lands onpackages/spec's public export surface; option 2 is a newscripts/consistency gate → devx), and guessing would inject a wrong value into the one field with a single producer. Recording this so later sweeps read the bare-lane state as intentional.Grading input for the findings round: option 1 only if the spec seat wants the wire-only vocabulary declared in the spec at all (a genuine question the card itself raises — these spellings exist precisely because no schema declares them); otherwise option 2. Neither is urgent: the set has been stable at four spellings, half of it is already pinned (
rest-server-repeated-filter-param.test.ts§5), and the drift only bites on a fifth spelling.
Generated by Claude Code
Triage: lands in
domain:spec; rationale: the load-bearing question is option 1 — whether the wire-only alias vocabulary belongs on@objectstack/spec's export surface at all — and that is the spec seat's call; option 2's cross-package gate is the fallback if the answer is no. Held asfinding(drift seam, nothing a user hits today, set stable at four spellings). Type: Task.
Generated by Claude Code
Finding-grading round: promoted to
pm:queue, re-routeddomain:spec→domain:spec-tooling, scoped to option 2 — acheck:script assertingrest's literal spelling set (query-multiplicity.ts:171-174) equals the normalizer's derived set (metadata-protocolWIRE_QUERY_ALIAS_SLOTS). Re-verified:@objectstack/metadata-protocolis a devDependency ofrest(no runtime import possible), set still exactly four, and open PR #8142 now documents "any of the four spellings" as contract — raising the cost of a silent fifth.Option 1 (hoist the wire vocabulary onto
@objectstack/spec's export surface) is deliberately not foreclosed — it's a spec-surface expansion question the spec seat can raise separately if wanted; the gate is reversible, adds no public surface, and pays the drift risk down now either way.
Generated by Claude Code
Claim:
domain:spec-toolingPM seat, sessionsession_01WocN37om5bw81JDoEEMA2e.- Branch:
claude/issue-8002-wire-alias-parity-gate - File surface: a new
scripts/check-*.mjsconsistency gate + its wiring (rootpackage.jsonscript, and the workflow entry if that is how the repo runs this family); readspackages/rest/src/query-multiplicity.tsandpackages/metadata-protocol/src/protocol.ts. ⛔ Option 1 (hoisting the wire vocabulary onto@objectstack/spec's export surface) is out of scope per triage — this card is scoped to option 2. - Container & model:
mode:subagent, M, modelclaude-opus-5(a new gate script is judgment work — what it compares and how it fails — but it grows no public API and changes no acceptance behavior, so no fable clause). - Premise re-verified at dispatch on
origin/main@a0fdc56:FILTER_SLOT_QUERY_PARAMSatpackages/rest/src/query-multiplicity.ts:171-174deriveswhere+ its spec aliases and then appends the two literals'filters','$filter';WIRE_QUERY_ALIAS_SLOTSis declared atpackages/metadata-protocol/src/protocol.ts:1689;@objectstack/metadata-protocolis inpackages/rest's devDependencies (workspace:*), so the no-runtime-import constraint the card rests on still holds. - Serial constraint: no other lane card holds
scripts/**right now; [finding] The metadata-layer arm of the visibility rules explains itself with*.form.ts, a file a Studio / MCP author of a schema-bound view does not have #8042 (in flight beside this one) is confined topackages/lintprose — zero overlap.
Generated by Claude Code
- Branch:
{ "issue": 8002, "status": "done", "branch": "claude/issue-8002-wire-alias-parity-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/8182", "premise_still_valid": true, "summary": "Implemented option 2: `scripts/check-filter-alias-parity.mjs`, a new `check:` gate asserting that the filter slot's wire spelling set is identical on both sides — `packages/rest`'s `FILTER_SLOT_QUERY_PARAMS` (spec-derived half + the literals `filters` / `$filter`) and `packages/metadata-protocol`'s `WIRE_QUERY_ALIAS_SLOTS` (spec table + its `extra` wire-only entries). Premise re-verified on origin/main@7c21fb2 (moved past the dispatch-time a0fdc56): `FILTER_SLOT_QUERY_PARAMS` at query-multiplicity.ts:171-174, `WIRE_QUERY_ALIAS_SLOTS` at protocol.ts:1689, metadata-protocol still a devDependency of rest — all three hold. The mechanism question was measured rather than assumed: a runtime import is impossible TWICE over — `WIRE_QUERY_ALIAS_SLOTS` is a module-private const (not exported, so there is nothing to import even inside the package), on top of the devDependency edge the card names — so the gate reads source through the TypeScript compiler API, which is this repo's existing house idiom for cross-package source truth (12 sibling root gates import `typescript`; regex was rejected because all three declarations are documented in prose quoting the spellings and two of the three are IIFEs). One design extension beyond the card: `WIRE_DOLLAR_ALIASES` folds `$x` into its bare spelling BEFORE the slot table folds, so a `$`-alias onto a filter spelling would itself be an ungated filter spelling; the normalizer side reads that table too, and the self-test pins the two-hop shape (it contributes nothing today, which is why it is worth pinning). Rot is handled as the primary failure mode: every reader asserts its anchor, so a moved declaration / changed initializer / non-literal element is a loud red naming the file, never an empty corpus reported as a pass (#4690). No duplication of `#7390 §5 filterSlotSpellingsAreComplete` — that pins the REST side alone; this covers the metadata-protocol half it cannot reach.", "tests": "Gate itself, on the real tree: `node scripts/check-filter-alias-parity.mjs` -> `check:filter-alias-parity: OK (4 wire spelling(s) of the \"where\" slot, identical on both sides: $filter, filter, filters, where)`; `--self-test` -> `passed (parity at four and at five spellings, both drift directions, the two-hop $ alias, four rot shapes, and the CI wiring)`. REVERSE VERIFICATION, planted on the REAL tree and reverted with `git checkout --` (never stash), all four directions measured: (a) fifth spelling `where_clause` in metadata-protocol's `extra.where` ONLY -> RED, exit 1, `FOLDED BUT UNGATED: where_clause` plus both file paths and the fix instruction — this is the direction #7390 §5 cannot see; (b) same spelling on BOTH sides -> GREEN, `5 wire spelling(s) ... identical on both sides`, i.e. the gate judges parity, not set size; (c) fifth spelling in rest's `FILTER_SLOT_QUERY_PARAMS` ONLY -> RED, `GATED BUT NOT FOLDED: where_clause`; (d) tree restored -> GREEN again, `git status` clean apart from the three intended paths. Gate families: re-derived against the ACTUAL changed paths with `node scripts/pm/dispatch-gates.mjs scripts/check-filter-alias-parity.mjs package.json .github/workflows/lint.yml` — the dispatch list's `check:authz-resolver` / `check:meta-type-normalized` fell away (the final diff touches no `packages/rest/src` file) and SIX families were added by the lint.yml edit; all run and green: `check:nul-bytes` (7428 files), `check:required-contexts`, `check:workflow-status-functions`, `check:node-version`, `check:shard-attestation`, `check:changeset-gate-self-tests` (388 assertions incl. its own lint.yml wiring reads), `check:type-check-coverage` (run under `flock /tmp/os-heavy-verify.lock`, `NODE_OPTIONS=--max-old-space-size=4096`). `npx eslint scripts/check-filter-alias-parity.mjs` clean (root eslint.config.mjs globs `**/*.mjs`). Control-byte self-scan over all three changed files: clean. No package `pnpm test` run: the final diff contains zero package source or test files (scripts/ + root package.json + lint.yml only), so there is no affected package suite; the gate's own 20-assertion self-test plus the four planted real-tree measurements are the evidence for this change. CI not yet read — reporting at draft-PR time per #6644 L2.", "open_questions": [], "out_of_scope_findings": [] }Notes the PM may want beyond the JSON:
- Meta-gate check (the dispatch brief asked me to look for one).
packages/spec'scheck:generated --reconcile-onlyis the repo'scheck:/gen:classification meta-gate, but it readspackages/spec/package.jsononly (pkgRoot, line 485) — a rootscripts/gate is outside its program, so no ledger entry is needed or possible. There is no meta-gate reconciling rootcheck:*scripts againstlint.yml; the house substitute is that individual gates self-test their own wiring (check-empty-changeset,check-changeset-no-majorboth do it), so this gate's--self-testasserts its own rootpackage.jsonentry and its singlelint.ymlstep. skip-changesetapplied and read back. Root manifest isprivate: trueand no published package changes, so this is the normal skip case. First union write was wiped one second later by the labeler bot (the test(service-queue): pin db-queue-adapter's engine double to ObjectQL.delete's dispatch predicate (#5198) #5533 race); re-read, re-wrote the union, and the settled set now reads["ci/cd","size/l","dependencies","skip-changeset"].- Option 1 mentioned in the PR body without a closing keyword, per triage, and the script's header records that if the hoist ever lands both sides become derived and this gate should be deleted rather than migrated.
Generated by Claude Code
Generated by Claude Code
- Meta-gate check (the dispatch brief asked me to look for one).
- added a commit that references this issue
on Aug 17, 2026
Observation-class finding, split out of #7390 while implementing it. Unassigned, filed per Prime Directive #10. Nothing a user hits today — this is a drift seam, not a live defect.
What the seam is
The filter slot has four wire spellings, and they are declared in two places with different ownership:
whereandfiltercome fromRPC_QUERY_ALIAS_SLOTS(packages/spec/src/data/data-engine.zod.ts), which its own docblock calls "the ONE place the alias to canonical mapping is declared".filtersand$filterare wire-only — no schema declares them.packages/metadata-protocolextends the spec table with them locally (WIRE_QUERY_ALIAS_SLOTS).#7390 added a third reader:
packages/restnow gates the filter slot's arity at the querystring ingress, and needs the same four spellings to do it. It deriveswhere/filterfrom the spec table, but cannot derivefilters/$filter—@objectstack/metadata-protocolis a dev-only dependency of@objectstack/rest, so no runtime import of that table exists. They are named literally inpackages/rest/src/query-multiplicity.ts(FILTER_SLOT_QUERY_PARAMS).The consequence
A new wire-only alias for the filter slot, added to
metadata-protocol's table, would fold correctly in the normalizer and be silently ungated at the ingress — repetition on that spelling would fall back to the misdiagnosis #7390 exists to remove. Nothing fails: the spelling works, the gate simply does not see it.Half of it is already pinned:
rest-server-repeated-filter-param.test.ts§5 asserts the derived set composes to exactly the four spellings, so a spec-table change goes red. The wire-only half has no such backstop, because the package holding the truth is not on the runtime graph.Symmetric note:
WIRE_DOLLAR_ALIASESin the same normalizer already carries a comment about exactly this class of mistake — "Adding a$alias in one place and not the other is exactly how a parameter ends up folded but unchecked." That sentence is now true across a package boundary as well as within one.Options
@objectstack/spec— e.g. aWIRE_ONLY_QUERY_ALIASESexport besideRPC_QUERY_ALIAS_SLOTS— so bothmetadata-protocolandrestread one table. Cleanest, and the only option that removes the seam rather than watching it. It touches the spec's public export surface (check:api-surfaceregeneration), and whether the wire vocabulary belongs in the spec at all is a genuine question: these spellings exist precisely because no schema declares them.check:script assertingrest's literal set equals the normalizer's derived set. No dependency-graph change; adds a script whose only job is to watch two lists.Recommendation is (1) if the spec seat wants the wire vocabulary declared at all, else (2). Both are cheap; neither is urgent.
Dedup
Searched open issues for
WIRE_QUERY_ALIAS_SLOTS,RPC_QUERY_ALIAS_SLOTS, wire alias,$filter,filtersplural, and query-alias drift. Hits: #7390 (the parent), #4436, #3774 — none covers the two-package declaration of the wire-only spellings.Generated by Claude Code