Skip to content

finding: the wire-only filter alias spellings (filters / $filter) are now declared in two packages, with no gate reconciling them #8002

Description

@hotlong

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:

  • where and filter come from RPC_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".
  • filters and $filter are wire-only — no schema declares them. packages/metadata-protocol extends 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

  1. Hoist the wire-only spellings into @objectstack/spec — e.g. a WIRE_ONLY_QUERY_ALIASES export beside RPC_QUERY_ALIAS_SLOTS — so both metadata-protocol and rest read 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-surface regeneration), and whether the wire vocabulary belongs in the spec at all is a genuine question: these spellings exist precisely because no schema declares them.
  2. A cross-package consistency gate — a check: script asserting rest's literal set equals the normalizer's derived set. No dependency-graph change; adds a script whose only job is to watch two lists.
  3. 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

  1. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    Triage: stays finding, deliberately left without domain:* — the lane is decided by the chosen shape (option 1 lands on packages/spec's public export surface; option 2 is a new scripts/ 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

  2. added theissue type on Aug 12, 2026
  3. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    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 as finding (drift seam, nothing a user hits today, set stable at four spellings). Type: Task.


    Generated by Claude Code

  4. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    Finding-grading round: promoted to pm:queue, re-routed domain:spec → domain:spec-tooling, scoped to option 2 — a check: script asserting rest's literal spelling set (query-multiplicity.ts:171-174) equals the normalizer's derived set (metadata-protocol WIRE_QUERY_ALIAS_SLOTS). Re-verified: @objectstack/metadata-protocol is a devDependency of rest (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

  5. self-assigned this
    on Aug 12, 2026
  6. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    Contributor

    Claim: domain:spec-tooling PM seat, session session_01WocN37om5bw81JDoEEMA2e.

    • Branch: claude/issue-8002-wire-alias-parity-gate
    • File surface: a new scripts/check-*.mjs consistency gate + its wiring (root package.json script, and the workflow entry if that is how the repo runs this family); reads packages/rest/src/query-multiplicity.ts and packages/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, model claude-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_PARAMS at packages/rest/src/query-multiplicity.ts:171-174 derives where + its spec aliases and then appends the two literals 'filters', '$filter'; WIRE_QUERY_ALIAS_SLOTS is declared at packages/metadata-protocol/src/protocol.ts:1689; @objectstack/metadata-protocol is in packages/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 to packages/lint prose — zero overlap.

    Generated by Claude Code

  7. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    Contributor
    {
      "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's check:generated --reconcile-only is the repo's check:/gen: classification meta-gate, but it reads packages/spec/package.json only (pkgRoot, line 485) — a root scripts/ gate is outside its program, so no ledger entry is needed or possible. There is no meta-gate reconciling root check:* scripts against lint.yml; the house substitute is that individual gates self-test their own wiring (check-empty-changeset, check-changeset-no-major both do it), so this gate's --self-test asserts its own root package.json entry and its single lint.yml step.
    • skip-changeset applied and read back. Root manifest is private: true and 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions