Skip to content

fix(tooling): walk the references/ subtrees in check:skill-frame-sync - #15060

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-15056-frame-sync-scan-references
Sep 3, 2026
Merged

fix(tooling): walk the references/ subtrees in check:skill-frame-sync#15060
os-steve merged 1 commit into
mainfrom
claude/issue-15056-frame-sync-scan-references

Conversation

@claude

@claude claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #15056

check:skill-frame-sync has two halves. The isomorphism half compares the two declared copies of the escalation decision frame. The anti-dormancy half walks .claude and skills looking for a third copy nobody declared — the half the gate's own header names as "what keeps a dev-side copy from quietly coming back". That second half could not see any references/ subtree, so 32 of the 72 markdown files under its own two scan roots (44%) were never opened: the 20 internal PM protocol files under .claude/skills/pm-dispatch/references/, and all 12 published skill reference pages that ship verbatim to third parties. The gate reported 40 markdown files scanned and was green about a population it had never read.

The reason the skip was there, and why it does not hold here

references sat in SCAN_SKIP_DIRS beside node_modules, .git and dist. The file states no reason of its own — the docblock at the neighbouring constant is about ROOT_DIR_WATCH_HINTS, a different subject. The idiom is shared with this repo's prose gates over the docs corpus, and one of them states the reason outright, in scripts/check-corpus-claim-drift.mjs:

Generated references/ is skipped: the spec source is the fix site there, so a finding in a generated file names the wrong file.

That sentence is about content/docs/references/, which is generated from spec. check-skill-frame-sync never walks content/docs. Under its two scan roots the same directory name holds hand-written operative prose, including decision-analysis.md, whose subject matter is escalation analysis and which is therefore the single likeliest place for a third copy to be pasted. So the reason does not hold for this gate's population, and the entry is removed rather than narrowed.

Two sibling gates over this same corpus already carry the corrected verdict:

  • scripts/check-skill-identifier-liveness.mjs keeps references out of its skip set and pins the difference in its self-test: "under skills/** those files are hand-authored published content, unlike content/docs/references which is generated".
  • scripts/check-doc-authoring.mjs gave its published-catalog rule a second walk rather than reuse the skipping one, because reusing it "would have produced a gate that runs, passes, and cannot see a ninth of the population it exists to guard".

There is deliberately no narrowed carve-out for the generated pages under skills/** (references/_index.md, references/react-blocks.md) either. That is the same refusal check-doc-authoring.mjs records beside those two files — "an exemption over a surface that no longer needs one is where the next regeneration would smuggle one back in. A red here is fixed AT THE SPEC SOURCE, never by hand-editing the artifact" — and it holds a fortiori here: a fingerprint hit in a generated page would mean the generator is emitting a copy of the frame, which is a real finding whose fix site is the generator, not a false one.

Before and after — the gate's own verdict lines

Before (node scripts/check-skill-frame-sync.mjs, exit 0):

✓ check-skill-frame-sync: 2 copies of the decision frame are structurally isomorphic across 2 files
  4 axes: business-need → long-term-soundness → ai-authoring-safety → startup-scope-discipline
  binding sentence present in all 2; 5 count mention(s) agree; 40 markdown files scanned for undeclared copies.

After (exit 0):

✓ check-skill-frame-sync: 2 copies of the decision frame are structurally isomorphic across 2 files
  4 axes: business-need → long-term-soundness → ai-authoring-safety → startup-scope-discipline
  binding sentence present in all 2; 5 count mention(s) agree; 72 markdown files scanned for undeclared copies.

2 declared copies before and after, 0 undeclared copies over the widened population: the widened scan finds no fourth copy of the frame in the 32 newly-visible files, which matches the filer's "not asserted" section. So no markdown file is edited by this PR, and none needed to be.

The population is pinned from the walk, not as a count

The self-test gains a scan-population block. It calls the same walkMarkdown over SCAN_ROOTS that main() does and asserts three things, all derived:

  1. the walk reaches the references/ subtrees at all (restoring the skip empties this),
  2. it reaches more than those subtrees, so case 1 is judging a population rather than a coincidence,
  3. every SCAN_SKIP_DIRS entry is declared installed-or-generated in the self-test, so a fourth entry has to be stated rather than appended.

A hand-typed file count was rejected: it would have to be re-typed whenever a reference page is added, and it cannot state the thing that actually went wrong — "the scan reaches no reference page at all". The self-test prints the derived numbers instead:

✓ check-skill-frame-sync self-test: 13 cases pass, plus 5 dispatch-gates declaration cases and 3 scan-population cases (72 markdown files walked under the scan roots, 32 of them inside a references/ directory).

Ablation — the pin fails when the skip returns

Run from the committed state (ffb0f544), with an absolute-path trap ... EXIT INT TERM restore. No rebuild leg applies: this gate is a plain .mjs run directly by package.json, so there is no dist/ between the edit and the run.

Mutation proved on disk before anything was measured — anchored on the exact line, both directions, plus a blob comparison against the HEAD blob:

HEAD_BLOB=53f2d98858f0a7da39c5e7bdd3ef83a02bdcfe16
MUTATION PROOF: injected_line_count=1 removed_line_count=0 mut_blob=e6bd417339149ab9274423b8a3dcc1ecd5bafd8e

On the mutated tree the self-test exits 1 and both pins name themselves:

✗ scan population: the walk reaches the `references/` subtrees under SCAN_ROOTS — hand-written operative prose, not an installed or generated tree. Restoring the skip empties this and every gate stays green, which is the defect this case exists for
✗ scan population: every SCAN_SKIP_DIRS entry is DECLARED installed-or-generated here — the criterion the three surviving entries meet and `references` never did, so a fourth entry has to be stated rather than appended
✗ check-skill-frame-sync self-test failed (2 case(s)).

The direction worth naming: on that same mutated tree the gate itself stays GREEN, at 40 markdown files scanned, exit 0. That is exactly the dormancy shape this card is about — losing 32 files costs nothing visible — and it is why the pin had to be added rather than relying on the gate's own verdict.

Restore leg proved by state, not by exit code: git checkout HEAD -- ... (never the bare form, which reads from the index), then git status --porcelain empty, git diff HEAD --name-only empty, and git hash-object back to 53f2d98858f0a7da39c5e7bdd3ef83a02bdcfe16, equal to the HEAD blob.

Gates

Re-derived after the commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — the script takes its own change set from the merge base): 22 families, identical to the dispatched list, no additions. All exit codes captured by redirect before any pipe. Verification run head sha: ffb0f544 (working tree clean against it for every run below).

gate verdict line
pnpm check:skill-frame-sync ✓ check-skill-frame-sync self-test: 13 cases pass, plus 5 dispatch-gates declaration cases and 3 scan-population cases (72 markdown files walked under the scan roots, 32 of them inside a references/ directory). + ✓ check-skill-frame-sync: 2 copies ... 72 markdown files scanned for undeclared copies.
pnpm check:pm-dispatch-gates ✓ dispatch-gates self-test: 1288 cases pass.
node scripts/pm/bare-root-worklist.mjs --self-test OK self-test: 57 live row(s), 49 unreachable as spelled, 49 recorded verdict(s) — none stale, none missing, none contradicted.
pnpm check:watch-hint-literal ✓ check-watch-hint-literal: 48 declaration(s) across 4 rostered name(s) ... no unrostered spelling of the idiom in the tree.
pnpm check:entry-guard ✓ check:entry-guard: 205 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 152 export bindings, 152 of them inert on import
pnpm check:parse-guard ✓ check:parse-guard: 204 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
pnpm check:nul-bytes ✓ check-nul-bytes --self-test: 75 assertions over a temp git repo (real scan() path)
pnpm check:ratchet-remedy-authority OK check-ratchet-remedy-authority: 186 scripts swept ...
pnpm check:agent-test-spelling ✓ check-agent-test-spelling: 0 violations — 436 file(s) ...
pnpm check:bash32-floor ✓ check-bash32-floor: 26 tracked shell file(s) ...
pnpm check:cli-command-ids ✓ check-cli-command-ids: 324 command-id literal(s) across 114 file(s) ... all resolve
pnpm check:pnpm-filter-targets ✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 33 file(s) resolve
pnpm check:refd-timer-probe ✓ check-refd-timer-probe self-test: 11 cases pass, negative controls included.
pnpm check:cross-package-test-inputs OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
node scripts/check-cross-package-test-inputs.mjs same verdict line as above
node scripts/check-ci-filter-parity.mjs OK: all 135 declared cross-package glob(s) (95 unique) are covered by core or crosspkg ...
node scripts/check-closing-keyword-parity.mjs check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords ... all registered).
node scripts/check-comment-mask-corpus.mjs ✓ comment-mask corpus sweep: 5843 files, 0 disagree, 0 unparseable
node scripts/check-self-test-wired.mjs ✓ check-self-test-wired: every one of the 166 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-shard-attestation.mjs ✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
node scripts/check-whole-set-label-write.mjs ✓ check-whole-set-label-write: 0 violations — 256 file(s) over 3 root(s) ...
node scripts/check-test-completeness.mjs NOT MEASURED (exit 3), in the gate's own words: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named. It also says of exactly this path: "the local reading for this gate is NOT MEASURED. It is not a red, and there is nothing here to fix."

Repo-wide ESLint over the whole tree, not a narrowing: pnpm lint (eslint . --no-inline-config), exit 0, no findings — held the shared verification lock 63s.

The two heavy runs (check:pm-dispatch-gates, pnpm lint) went through bash scripts/pm/os-verify-lock.sh -c ...; both printed VERDICT command-exit 0.

Not a governed surface: node scripts/pm/check-governed-merges.mjs --test scripts/check-skill-frame-sync.mjs prints governed-surface predicate: 0 of 1 path(s) hit the register on this final file list. No changeset — the diff publishes nothing from any released package, so skip-changeset applies.


Generated by Claude Code

The anti-dormancy scan skipped every directory named `references`, an idiom
carried over from the prose gates over `content/docs`, where that directory is
generated from spec. This gate never walks `content/docs`; under its two scan
roots (`.claude`, `skills`) the same name holds hand-written operative prose,
so the skip hid 32 of 72 markdown files - 44% of the population - from the half
of the gate that exists to notice a new copy of the decision frame.

Drop `references` from SCAN_SKIP_DIRS, with the reason it was there and why it
does not hold recorded on the set. No narrowed carve-out for the generated
pages under `skills/**`: a fingerprint hit there would mean the generator emits
a copy, which is a real finding whose fix site is the generator.

Pin the population in the self-test from the walk's own output rather than as a
count, so restoring the skip goes red and names itself.

Before: 2 copies isomorphic, 40 markdown files scanned.
After:  2 copies isomorphic, 72 markdown files scanned, 0 undeclared copies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants