test(objectql,runtime): widen the deleted-member absence pin from one file to the tree - #15085
Conversation
… file to the tree PR #14667 deleted the private `ObjectQLPlugin.actionObjectKey` and wrote a guard for it: `expect(plugin!.text).not.toContain('actionObjectKey')`. The kind of guard was right; its SCOPE was the defect. A pin written by the deleting PR can only look where its author thought to look, and the whole failure mode is references the author did not know about — five files in three other packages went on naming the dead member as a live reader, and one deletion produced two separate follow-up cards. Both `action-owner-key-single-source.test.ts` pins now assert the absence tree-wide instead: one `git grep` over `.ts` under `packages/` and `examples/`, tracked plus untracked, with three exclusion rules carried in the pin beside their reasons (published CHANGELOGs, `.changeset/`, and the two pins themselves, which name the member because naming it is how they hunt for it). Widening also covers the half no removal-time check can see. Three of the five references existed when the member died; the other two were written 1 h 41 min AFTER it, by a later PR, into a file that was clean at deletion time. A pin that runs on every PR reddens on that second kind at the moment it is written. Also in this change: - `packages/spec/src/stack.zod.ts` — two comments naming the dead member as the registration-key reader TODAY now name `standaloneActionOwnerKey`, the live helper. Only that one word rots; `collectBundleActions` beside it is alive. - `packages/objectql/src/action-governance.ts` — accurate history, reworded so it no longer carries the dead name. - `scripts/cross-package-test-inputs.mjs` and `turbo.json` — the declaration a tree-scoped test owes. Without it neither the affected-subset filter nor the turbo cache re-runs these suites for the files they now judge, which is the blind spot `check:cross-package-test-inputs` exists to close. ⛔ An assertion of absence is not a stale mention. The pins name the member on purpose; "repairing" those lines deletes the guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
📓 Docs Drift Check1 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 129 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6d5392930850c758966962417db85cb2d3066b41 && git checkout 6d5392930850c758966962417db85cb2d3066b41
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6665c5c916caf6d95022734328ae82154e1a5f10 c754845e33b47492835e51905ad040f95d0a4a4f && git checkout -B drift-repro 6665c5c916caf6d95022734328ae82154e1a5f10 && git merge --no-ff c754845e33b47492835e51905ad040f95d0a4a4f
node scripts/docs-audit/affected-docs.mjs --json 6665c5c916caf6d95022734328ae82154e1a5f10 |
…gate farm owns
`pnpm check:pm-dispatch-gates` went red on the previous commit — reproduced
locally, 1 of 1291 self-test cases failed, exit 1:
✗ but no hint of this gate reaches a test file outside packages/**
Root cause, measured rather than guessed. `scripts/cross-package-test-inputs.mjs`
is a declaration table that `check-cross-package-test-inputs.mjs` imports, and
`dispatch-gates.mjs` appends a followed module's globs to every importer as watch
hints. So the `examples/**/*.ts` glob the previous commit added became an
inherited hint on that gate — and the self-test pins that no hint of it reaches a
test file outside `packages/**`, which is the whole reason the gate is listed as
a change-KIND rather than a path derivation.
That case is not a count to bump. All 41 tracked test files outside `packages/`
are under `examples/`, so one examples-wide glob does not shrink the residue
class it guards, it EMPTIES it — and the case's own instruction ("re-point at
another member of its class") cannot be followed because no other member exists.
Editing it would be weakening a gate to fit a declaration, in a `scripts/pm/`
file this change does not own.
So the pin narrows instead: `SCANNED_ROOTS` is `packages/` alone, and the
declared glob drops to `packages/**/*.ts`, which the table already carried for
`@objectstack/core` and `@objectstack/types` — the hint population is now
byte-identical to `origin/main`'s and the census does not move at all.
⚠️ What that costs is written into both pins rather than left to be rediscovered:
of this symbol's five surviving references, four were under `packages/**` and one
was a test under the showcase example, which this pin no longer sees. Widening
needs the residue measurement behind that self-test case redone first; the
headers say so, and say that editing the case is not the repair.
The reach control moves with the scope — from the showcase test to
`packages/cli/src/commands/lint.ts`, which is also one of the files that carried
the dead name until it was repaired, so it still proves the scan leaves its home
package.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Fixes #14878
Fixes #14873
Implements ruling D (comment 5528359266, decision batch 22, maintainer 「同意」), which withdraws the earlier ruling B on the same card. ⛔ No new gate, no
scripts/check-removed-symbol-mentions.mjs, no parser, no marker vocabulary — none of that is here.What was wrong
PR #14667 deleted the private
ObjectQLPlugin.actionObjectKeyand it did write a guard:expect(plugin!.text).not.toContain('actionObjectKey'). The right kind of guard with the wrong scope. A pin written by the deleting PR can only look where its author thought to look, and the whole failure mode is references the author did not know about — so it stayed green while five files in three other packages went on naming the dead member as a live reader, and one deletion produced two triage cards.Widening it also covers the half that nothing keyed on the deleting diff can see. Three of those five references already existed when the member died; the other two were written 1 h 41 min after it, by a later PR, into a file that was clean at deletion time. A pin that runs on every PR reddens on that second kind at the moment it is written.
The change
The two pins go tree-scoped.
packages/objectql/src/action-owner-key-single-source.test.tsandpackages/runtime/src/action-owner-key-single-source.test.tseach replace the one-file negative with onegit grepover.tsunderpackages/, tracked plus untracked (so a file written but not yetgit added still reddens locally, rather than in the merge queue).Three exclusion rules live in the pin, each with its reason written beside it — ⛔ never an allowlist file:
**/CHANGELOG.md.changeset/It is carried in both packages on purpose. Either copy alone catches everything; the redundancy is against the one failure this card already demonstrated — a guard disappearing with the file that held it. Sharing it through a helper module would undo exactly that, and would put new always-loaded machinery in
scripts/for a pin.Every surviving mention was classified before it was touched, which is the trap on this card:
packages/spec/src/stack.zod.ts:381,:1574standaloneActionOwnerKey, the live helper. Only that one word rots:collectBundleActionsbeside it is alive (packages/runtime/src/app-plugin.ts:1878)packages/objectql/src/action-governance.ts:78:6docblock,:69test title:10docblockexpect(plugin!.text).not.toContain('actionObjectKey')plugin.tsCHANGELOG.md, 2 ×.changeset/*.mdThe declaration a tree-scoped test owes —⚠️ This is outside the file face the dispatch declared, and it is not optional:
scripts/cross-package-test-inputs.mjsandturbo.json.pnpm check:cross-package-test-inputsfails the PR without it, and without it neither the affected-subset filter nor the turbo cache re-runs these suites for the files they now judge — the exact blind spot that gate exists to close, which would leave the widened pin green onmainthrough the drift it is for. Cost, stated rather than discovered later:@objectstack/objectql#testand@objectstack/runtime#testnow hashpackages/**/*.ts, the radius@objectstack/coreand@objectstack/typesalready declare for pins of this shape, so those two suites run on most PRs instead of on graph-affected ones. The hint population that table feeds is byte-identical toorigin/main's — see the section below for why that is load-bearing.Why the scan stops at
packages/, and what it costsThe first commit scanned
examples/too — the right radius on the evidence, since one of the five surviving references lived there — andpnpm check:pm-dispatch-gateswent RED on it. Reproduced locally, exit 1, 1 of 1291 self-test cases:Root cause, measured.
scripts/cross-package-test-inputs.mjsis a declaration table thatcheck-cross-package-test-inputs.mjsimports, anddispatch-gates.mjsappends a followed module's globs to every importer as watch hints. An examples-wide.tsglob therefore became an inherited hint on that gate — and that self-test case pins that no hint of it reaches a test file outsidepackages/**, which is the whole reason the gate is listed as a change-KIND rather than a path derivation.⛔ That case is not a count to bump. All 41 tracked test files outside
packages/are underexamples/, so one examples-wide glob does not shrink the residue class it guards — it empties it, and the case's own instruction ("re-point at another member of its class") cannot be followed because no other member exists. Editing it would be weakening a gate to fit a declaration, in ascripts/pm/file this change does not own.So the pin narrowed instead, and the price is written into both headers rather than left to be rediscovered: four of the five surviving references were under
packages/**; the fifth was a test under the showcase example, which this pin does not see. Widening needs the residue measurement behind that self-test case redone first — that is a follow-up for thescripts/pm/lane, not an edit to make here.The ablation below measures the narrowing in both directions rather than asserting it.
Evidence — the pin is shown FAILING, not merely passing
A pin you cannot show failing is not evidence. Ablation on the committed implementation (mutate, prove on disk, run, restore, prove restored by state):
Four things that ablation establishes at once: the pin reds on a stale mention; it leaves its own package (the site is
packages/cli, one of the files that carried the dead name until it was repaired); it reddens on an untracked file, which is the #14686 shape — a later author writing a fresh mention of an already-dead symbol, the shape a removal-time check is structurally blind to; and theexamples/boundary is real rather than claimed, since the out-of-scope twin is not reported. The other 4 and 15 assertions stayed green throughout, so the absence-assertion inside the pin was never flagged as its own offender.Anti-vacuity is pinned in the file too, because a grep that matches nothing yields the same empty violation set as a clean repo: each pin asserts the scan can see the dead name in its own file, and that it reaches
packages/cli/src/commands/lint.tsand the sibling package.Verification — all at
c754845e3, the final commitGate family from
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatc754845e3— 64 commands, re-derived after the edits and identical at every commit on this branch. Exit codes captured before any pipe.60 green. Four could not be measured in this worktree, none of them a finding:
check-dev-prereqscheck-test-completenessturbo run testlog; its own text says to record it as NOT MEASURED when run from the familycheck:dual-build-cjs-loadscheck:type-check-debt⛔ Exit 3 is NOT MEASURED: never a pass and never a red.
check:pm-dispatch-gatesis not in that derived family — it is part of the always-runs tail the derivation prints a warning about and deliberately does not list. That is how the first commit reached CI red: the family was green and the gate that caught this was never in it. It is now run explicitly, as the exact CI command and detached, as its own header instructs on an agent container:Not in this PR
The ruling's fourth bullet — one sentence in the retirement playbook's deleting-PR checklist saying the absence pin a deleting PR writes is tree-scoped — lives under
.claude/skills/**, a governed surface that merges by hand. The ruling allows it to travel separately, so it is its own draft PR rather than dragging this change onto the human-merge path for one sentence.🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code