Skip to content

test(objectql,runtime): widen the deleted-member absence pin from one file to the tree - #15085

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-14878-tree-scoped-absence-pin
Sep 3, 2026
Merged

test(objectql,runtime): widen the deleted-member absence pin from one file to the tree#15085
baozhoutao merged 2 commits into
mainfrom
claude/issue-14878-tree-scoped-absence-pin

Conversation

@claude

@claude claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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.actionObjectKey and 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.ts and packages/runtime/src/action-owner-key-single-source.test.ts each replace the one-file negative with one git grep over .ts under packages/, tracked plus untracked (so a file written but not yet git 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:

Rule Reason
**/CHANGELOG.md a published CHANGELOG is the record of the removal
.changeset/ the same record before it is compiled into a CHANGELOG
the two pin files they carry the name as their own search string — "repairing" them deletes the guard

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:

Site Verdict Action
packages/spec/src/stack.zod.ts:381, :1574 live claim — present tense, "the registration key … read" renamed to standaloneActionOwnerKey, the live helper. Only that one word rots: collectBundleActions beside it is alive (packages/runtime/src/app-plugin.ts:1878)
packages/objectql/src/action-governance.ts:78 accurate history reworded to "a private owner-key method on the ObjectQL plugin" — the history survives, the dead name does not
objectql pin :6 docblock, :69 test title history inside the pin pin file, excluded by construction
runtime pin :10 docblock history inside the pin pin file, excluded by construction
expect(plugin!.text).not.toContain('actionObjectKey') assertion of absence — it IS the guard not repaired. Widened: the same negative now reads the tree instead of plugin.ts
4 × CHANGELOG.md, 2 × .changeset/*.md release record excluded by rule

The declaration a tree-scoped test owesscripts/cross-package-test-inputs.mjs and turbo.json. ⚠️ This is outside the file face the dispatch declared, and it is not optional: pnpm check:cross-package-test-inputs fails 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 on main through the drift it is for. Cost, stated rather than discovered later: @objectstack/objectql#test and @objectstack/runtime#test now hash packages/**/*.ts, the radius @objectstack/core and @objectstack/types already 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 to origin/main's — see the section below for why that is load-bearing.

Why the scan stops at packages/, and what it costs

The first commit scanned examples/ too — the right radius on the evidence, since one of the five surviving references lived there — and pnpm check:pm-dispatch-gates went RED on it. Reproduced locally, exit 1, 1 of 1291 self-test cases:

✗ but no hint of this gate reaches a test file outside packages/**

Root cause, measured. 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. An examples-wide .ts glob therefore became an inherited hint on that gate — and that self-test case 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 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 the scripts/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):

MUTATION (two NEW untracked files, both naming the member as live — one IN the
scan surface, one deliberately OUTSIDE it):
  packages/cli/src/ablation-14878-stale-mention.ts             grep -c actionObjectKey -> 1
  examples/app-showcase/src/ablation-14878-outside-scope.ts    grep -c actionObjectKey -> 1
  git status --porcelain -> both listed as ??

objectql pin  EXIT=1   Tests  1 failed | 4 passed (5)
runtime pin   EXIT=1   Tests  1 failed | 15 passed (16)
  both reports name the in-scope file and ONLY it:
    - packages/cli/src/ablation-14878-stale-mention.ts:2

RESTORE proven by observed state, never by an exit code:
  both files absent · git status --porcelain empty · git diff HEAD empty

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 the examples/ 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.ts and the sibling package.

Verification — all at c754845e3, the final commit

pnpm --filter @objectstack/objectql exec vitest run   268 files / 4611 tests   PASS
pnpm --filter @objectstack/runtime  exec vitest run   220 files / 3160 tests   PASS
pnpm --filter {objectql,runtime,spec} run typecheck   EXIT=0
    check:test-typecheck OK for all three — the edited test files ARE in the
    checked program (44 / 27 / 54 files, ledgers unchanged)
pnpm lint (eslint . --no-inline-config, whole repo)   EXIT=0   102s
pnpm --filter @objectstack/spec run check:authorable-surface   EXIT=0
    and `git status --porcelain` empty afterwards — the comment-only stack.zod.ts
    edit moves NO generated baseline, which answers the open question #14873 raised

Gate family from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at c754845e3 — 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:

Gate Code Reading
check-dev-prereqs 1 "The workspace is not built — 1 unmet precondition, not a list of problems" (only the dependency closure was built here)
check-test-completeness 3 needs a saved turbo run test log; its own text says to record it as NOT MEASURED when run from the family
check:dual-build-cjs-loads 3 PREREQUISITE NOT MET — reads built output
check:type-check-debt 3 PREREQUISITE NOT MET — refuses to measure without the built closure

⛔ Exit 3 is NOT MEASURED: never a pass and never a red.

⚠️ check:pm-dispatch-gates is 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:

pnpm check:pm-dispatch-gates   EXIT=0   ✓ dispatch-gates self-test: 1291 cases pass.

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

… 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
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 3, 2026
@github-actions github-actions Bot added the size/l label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/action-governance.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/action-governance.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 129 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6665c5c916caf6d95022734328ae82154e1a5f10packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6d5392930850c758966962417db85cb2d3066b41 — the merge of head c754845e33b47492835e51905ad040f95d0a4a4f into base 6665c5c916caf6d95022734328ae82154e1a5f10, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants