test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616) - #14672

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin
Sep 2, 2026
Merged

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616)#14672
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14616

One check:pm-dispatch-gates self-test case, in the tier region of selfTest() in scripts/pm/dispatch-gates.mjs. Nothing else in the tree moves: the constant is untouched, .claude/skills/pm-dispatch/** is untouched, scripts/pm/check-skill-line-ratchet.mjs is untouched. One file, two commits (the second is the fix for a shadowed import the first battery found — see "What the first run measured" below).

The case

What it walks. Two roots, both DERIVED rather than spelled:

  • the pm-dispatch skill tree — dirname() of the mandate glob that names the skill's main file, taken from MANDATORY_TIER_GLOBS, a population this module already declares;
  • this tool's own directory — its own module path, taken relative to ROOT.

At this commit that resolves to .claude/skills/pm-dispatch + scripts/pm, 21+17 files. Derivation is not decoration: the population follows a rename instead of rotting into a scan that reaches nothing, and a mandate glob renamed away leaves an empty root, which the walk reports as a failure rather than quietly walking the whole tree.

What it counts. Occurrences of the tier constant's own VALUE, read from the constant at run time. Never a second literal — that spelling would itself BE the extra site the case exists to forbid — and never a family or prefix pattern: a prefix scan would be a family floor the human floor has not granted, and would itself be a second spelling of the model family inside this tool, i.e. the defect recreated by its own guard.

What it reports. Every occurrence as file:line, in the case name, next to the definition site it expected and the per-root file counts. A bare count is not actionable; what a reader gets on failure is the offending list. It is one t() call, non-vacuous by construction: it fails if either root scanned zero files, if the definition line cannot be found, if the count is not 1, or if the one site is not the definition line. The definition line is FOUND (the single line carrying both the constant's name and its value), never written down, so the case keeps no hand-maintained line number of its own.

Its green line, from the run below:

✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)

The two guards named in the filing, both measured

1. Watch-hint extraction and the declared population. This module's own extractWatchHints run over its own source, before and after the edit:

before (2aa8456c): 23 hints
after (da479d10): 23 hints — identical sets, element for element

The case adds no path literal to that set, for two independent reasons, and the belt-and-braces is deliberate: the roots are derived rather than spelled at all, and maskSelfTests blanks the selfTest() body before extraction, which is what makes any fixture path in that region inert in the first place. The module's inherited-population declaration is untouched and still true — declaredInheritedPopulation() over the edited source still answers .github/workflows and nothing else, so a follower inherits exactly what it inherited before.

The two gates that guard this class agree, each quoted from its own verdict line:

✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 29, ROOT_FILE_WATCH_HINTS 9, ROOT_WATCH_HINTS 2, DECLARED_WATCH_HINTS 5 -- every one an array of quoted literals inside its own statement, every rostered name non-empty, and no unrostered spelling of the idiom in the tree.
✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).

2. The verbatim maintainer quotes stay out of reach. The rulings quoted in the skill's main file and in references/dispatch-runbook.md name the model FAMILY as prose, not this constant's value, so an exact-value scan does not match them — measured rather than assumed: the scan over both roots returns exactly one site, and neither quote is in it. The case's own comment records this as a boundary to keep, not an accident to fix later: a gate that could demand edits to a maintainer's recorded words is the wrong gate, so the case must not be widened until it would reach them.

Red then green — predict-then-mutate

Prediction, written before the run: a SECOND spelling of the value anywhere under the two roots reds exactly this one case, and the failure names the offending path. No build leg to get wrong — a plain .mjs script, read from source.

The mutation is a file inside the population carrying the value, written from the constant at run time (the mutation script never spells it either); the restore is its removal, proved by observing state and not by any step's exit code. All three legs ran back to back in one hold of the container's shared verify lock, at da479d10:

=== LEG 1: green on the committed implementation ===
LEG1_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.
=== LEG 2: mutate — a SECOND spelling of the value inside the population ===
MUT_PRESENT=yes
MUT_VALUE_HITS=1
MUT_BYTES=23
LEG2_EXIT=1
✗ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+18 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955, scripts/pm/.tier-value-site-mutation.tmp:1)
✗ dispatch-gates self-test: 1 of 1241 case(s) failed.
=== LEG 3: restore, then MEASURE the restored tree ===
MUT_ABSENT=yes
GIT_STATUS_LINES=0
GIT_DIFF_HEAD_LINES=0
LEG3_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.

Four things that pair proves rather than asserts. The mutation reached disk before anything was read (MUT_VALUE_HITS=1, 23 bytes) — the leg was not a no-op reading healthy output. The file count moved 21+17 to 21+18, so the walk really opened it. Exactly ONE of 1241 cases failed, so the mutation reddened this case and nothing else. And the restore leg was MEASURED, not assumed: the file is gone, the tree is clean by git status and by git diff HEAD, and the full battery is green again on the restored tree.

Case count, which is the other half of "one case": 1240 cases pass at 2aa8456c before the edit, 1241 cases pass after — one case added, none replaced.

What the first run measured (kept, because it is the interesting part)

The first battery on this branch died on the new case's first statement:

TypeError: relative is not a function

relative from node:path is bound to a fixture STRING inside selfTest(), thousands of lines above the new case, so the shadowing is invisible at parse time and node --check passes. The second commit takes the repo-relative spelling from ROOT instead — the prefix every sibling case in this battery already joins against — and reads its own source through import.meta.url. A ROOT prefix that stops holding leaves an empty root, which the walk reports rather than turning into a pass. The rest of the scope was then swept for the same trap: relative and resolve are the only two node:path names shadowed anywhere in that function, and this case uses neither now.

Gates

Family re-derived AFTER the edit from the tool itself (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no hand-written path list), run one at a time under the shared verify lock, each exit code captured before any pipe. Head sha for every row: da479d10 (git rev-parse --short HEAD in that run; the tree was clean and no commit followed).

gateexitits own verdict line
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass. (legs 1 and 3)
pnpm check:watch-hint-literal0✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.
pnpm check:declared-population-live0✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).
pnpm check:entry-guard0✓ check:entry-guard: 199 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 150 export bindings, 150 of them inert on import (0 known-unsafe, SHRINK-ONLY).
pnpm check:parse-guard0✓ check:parse-guard: 198 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered by core or crosspkg …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
pnpm check:cross-package-test-inputs0same verdict line as the direct invocation above
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
pnpm check:agent-test-spelling0✓ check-agent-test-spelling: 0 violations — 425 file(s) · 5645 bare -- token(s) · 1380 launcher-rooted run(s) · 9 separator(s) JUDGED …
pnpm check:bash32-floor0✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment …
pnpm check:cli-command-ids0✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path …
pnpm check:pnpm-filter-targets0✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 32 file(s) resolve against 79 workspace package(s) …
node scripts/check-nul-bytes.mjs0check-nul-bytes: OK (scanned 7999 text file(s) … no raw ASCII control bytes).
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

check:nul-bytes is not in the derived family; it is run on every edit by standing rule, and the diff was separately swept for raw control bytes before the first commit.

NOT MEASURED

  • node scripts/check-test-completeness.mjs — exit 3, prerequisite not met. Its own text: check-test-completeness: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named, and, for exactly this situation, running the family locally, record this gate as NOT MEASURED. It is not a red, and there is nothing here to fix. Recorded as NOT MEASURED, not as a failure and not as a pass.
  • Repo-wide pnpm lint — narrowed deliberately, with the narrowing's own evidence. CI's Lint and Repo Gates runs the whole farm regardless; locally this ran targeted, and the three readings a narrowing owes are all here: (1) the population came from git, not from a guess — git diff --name-only 2aa8456c HEAD returns exactly one path, scripts/pm/dispatch-gates.mjs; (2) the file count came from eslint's own --format json output, FILES_LINTED=1 with that same absolute path in it, so the run was not a green over zero files; (3) untouched files cannot move, because this repo's single eslint.config.mjs says of itself that it never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, so no verdict on a file this diff did not touch depends on this diff. Result: ESLINT_EXIT=0, ERRORS=0 WARNINGS=0.
  • CI itself. Reported at draft-PR time by contract; the gate jobs on this PR had not converged when this body was written.

Draft on purpose: scripts/pm/** is governed, so this stays draft and merges by hand; review requests are the PM seat's step. skip-changeset is applied — the diff is one self-test case in a script under scripts/pm/** and publishes nothing from any package, which is the shape scripts/check-empty-changeset.mjs enumerates for the label (It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like) -> ... apply the 'skip-changeset' label). The label was written as a union with what the PR already carried and read back afterwards: size/s, skip-changeset, nothing stripped.

Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

…e self-test
One case walks the two roots the promise covers — the pm-dispatch skill tree
and this tool's own directory, both DERIVED (the directory of the mandate glob
naming the skill's main file; this module's own directory) rather than spelled,
so the population follows a rename and no fresh path literal joins this file's
watch-hint set. It counts occurrences of the tier constant's own VALUE, read
from the constant at run time — never a second literal, never a family or
prefix pattern — and requires exactly one, at the definition line, reporting
every offending file:line when it is not.
Until now that promise was held by a git grep someone remembers to run; the
drift it exists against was sixteen spellings across these same two roots.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026 — with Claude
…wed import
`relative` from `node:path` is bound to a fixture STRING inside `selfTest()`,
a few thousand lines above the new case, so calling it there is a TypeError at
run time rather than a name error at parse time — measured: the battery died
on the case's first statement with `relative is not a function`, after 1240
green cases. The repo-relative spelling now comes from ROOT, which every
sibling case in this battery already joins against, and the own-source read
goes through `import.meta.url` directly. A ROOT prefix that stops holding
leaves an empty root, which the walk reports instead of turning into a pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616) - #14672

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin
Sep 2, 2026
Merged

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616)#14672
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14616

One check:pm-dispatch-gates self-test case, in the tier region of selfTest() in scripts/pm/dispatch-gates.mjs. Nothing else in the tree moves: the constant is untouched, .claude/skills/pm-dispatch/** is untouched, scripts/pm/check-skill-line-ratchet.mjs is untouched. One file, two commits (the second is the fix for a shadowed import the first battery found — see "What the first run measured" below).

The case

What it walks. Two roots, both DERIVED rather than spelled:

  • the pm-dispatch skill tree — dirname() of the mandate glob that names the skill's main file, taken from MANDATORY_TIER_GLOBS, a population this module already declares;
  • this tool's own directory — its own module path, taken relative to ROOT.

At this commit that resolves to .claude/skills/pm-dispatch + scripts/pm, 21+17 files. Derivation is not decoration: the population follows a rename instead of rotting into a scan that reaches nothing, and a mandate glob renamed away leaves an empty root, which the walk reports as a failure rather than quietly walking the whole tree.

What it counts. Occurrences of the tier constant's own VALUE, read from the constant at run time. Never a second literal — that spelling would itself BE the extra site the case exists to forbid — and never a family or prefix pattern: a prefix scan would be a family floor the human floor has not granted, and would itself be a second spelling of the model family inside this tool, i.e. the defect recreated by its own guard.

What it reports. Every occurrence as file:line, in the case name, next to the definition site it expected and the per-root file counts. A bare count is not actionable; what a reader gets on failure is the offending list. It is one t() call, non-vacuous by construction: it fails if either root scanned zero files, if the definition line cannot be found, if the count is not 1, or if the one site is not the definition line. The definition line is FOUND (the single line carrying both the constant's name and its value), never written down, so the case keeps no hand-maintained line number of its own.

Its green line, from the run below:

✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)

The two guards named in the filing, both measured

1. Watch-hint extraction and the declared population. This module's own extractWatchHints run over its own source, before and after the edit:

before (2aa8456c): 23 hints
after (da479d10): 23 hints — identical sets, element for element

The case adds no path literal to that set, for two independent reasons, and the belt-and-braces is deliberate: the roots are derived rather than spelled at all, and maskSelfTests blanks the selfTest() body before extraction, which is what makes any fixture path in that region inert in the first place. The module's inherited-population declaration is untouched and still true — declaredInheritedPopulation() over the edited source still answers .github/workflows and nothing else, so a follower inherits exactly what it inherited before.

The two gates that guard this class agree, each quoted from its own verdict line:

✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 29, ROOT_FILE_WATCH_HINTS 9, ROOT_WATCH_HINTS 2, DECLARED_WATCH_HINTS 5 -- every one an array of quoted literals inside its own statement, every rostered name non-empty, and no unrostered spelling of the idiom in the tree.
✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).

2. The verbatim maintainer quotes stay out of reach. The rulings quoted in the skill's main file and in references/dispatch-runbook.md name the model FAMILY as prose, not this constant's value, so an exact-value scan does not match them — measured rather than assumed: the scan over both roots returns exactly one site, and neither quote is in it. The case's own comment records this as a boundary to keep, not an accident to fix later: a gate that could demand edits to a maintainer's recorded words is the wrong gate, so the case must not be widened until it would reach them.

Red then green — predict-then-mutate

Prediction, written before the run: a SECOND spelling of the value anywhere under the two roots reds exactly this one case, and the failure names the offending path. No build leg to get wrong — a plain .mjs script, read from source.

The mutation is a file inside the population carrying the value, written from the constant at run time (the mutation script never spells it either); the restore is its removal, proved by observing state and not by any step's exit code. All three legs ran back to back in one hold of the container's shared verify lock, at da479d10:

=== LEG 1: green on the committed implementation ===
LEG1_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.
=== LEG 2: mutate — a SECOND spelling of the value inside the population ===
MUT_PRESENT=yes
MUT_VALUE_HITS=1
MUT_BYTES=23
LEG2_EXIT=1
✗ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+18 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955, scripts/pm/.tier-value-site-mutation.tmp:1)
✗ dispatch-gates self-test: 1 of 1241 case(s) failed.
=== LEG 3: restore, then MEASURE the restored tree ===
MUT_ABSENT=yes
GIT_STATUS_LINES=0
GIT_DIFF_HEAD_LINES=0
LEG3_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.

Four things that pair proves rather than asserts. The mutation reached disk before anything was read (MUT_VALUE_HITS=1, 23 bytes) — the leg was not a no-op reading healthy output. The file count moved 21+17 to 21+18, so the walk really opened it. Exactly ONE of 1241 cases failed, so the mutation reddened this case and nothing else. And the restore leg was MEASURED, not assumed: the file is gone, the tree is clean by git status and by git diff HEAD, and the full battery is green again on the restored tree.

Case count, which is the other half of "one case": 1240 cases pass at 2aa8456c before the edit, 1241 cases pass after — one case added, none replaced.

What the first run measured (kept, because it is the interesting part)

The first battery on this branch died on the new case's first statement:

TypeError: relative is not a function

relative from node:path is bound to a fixture STRING inside selfTest(), thousands of lines above the new case, so the shadowing is invisible at parse time and node --check passes. The second commit takes the repo-relative spelling from ROOT instead — the prefix every sibling case in this battery already joins against — and reads its own source through import.meta.url. A ROOT prefix that stops holding leaves an empty root, which the walk reports rather than turning into a pass. The rest of the scope was then swept for the same trap: relative and resolve are the only two node:path names shadowed anywhere in that function, and this case uses neither now.

Gates

Family re-derived AFTER the edit from the tool itself (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no hand-written path list), run one at a time under the shared verify lock, each exit code captured before any pipe. Head sha for every row: da479d10 (git rev-parse --short HEAD in that run; the tree was clean and no commit followed).

gateexitits own verdict line
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass. (legs 1 and 3)
pnpm check:watch-hint-literal0✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.
pnpm check:declared-population-live0✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).
pnpm check:entry-guard0✓ check:entry-guard: 199 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 150 export bindings, 150 of them inert on import (0 known-unsafe, SHRINK-ONLY).
pnpm check:parse-guard0✓ check:parse-guard: 198 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered by core or crosspkg …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
pnpm check:cross-package-test-inputs0same verdict line as the direct invocation above
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
pnpm check:agent-test-spelling0✓ check-agent-test-spelling: 0 violations — 425 file(s) · 5645 bare -- token(s) · 1380 launcher-rooted run(s) · 9 separator(s) JUDGED …
pnpm check:bash32-floor0✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment …
pnpm check:cli-command-ids0✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path …
pnpm check:pnpm-filter-targets0✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 32 file(s) resolve against 79 workspace package(s) …
node scripts/check-nul-bytes.mjs0check-nul-bytes: OK (scanned 7999 text file(s) … no raw ASCII control bytes).
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

check:nul-bytes is not in the derived family; it is run on every edit by standing rule, and the diff was separately swept for raw control bytes before the first commit.

NOT MEASURED

  • node scripts/check-test-completeness.mjs — exit 3, prerequisite not met. Its own text: check-test-completeness: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named, and, for exactly this situation, running the family locally, record this gate as NOT MEASURED. It is not a red, and there is nothing here to fix. Recorded as NOT MEASURED, not as a failure and not as a pass.
  • Repo-wide pnpm lint — narrowed deliberately, with the narrowing's own evidence. CI's Lint and Repo Gates runs the whole farm regardless; locally this ran targeted, and the three readings a narrowing owes are all here: (1) the population came from git, not from a guess — git diff --name-only 2aa8456c HEAD returns exactly one path, scripts/pm/dispatch-gates.mjs; (2) the file count came from eslint's own --format json output, FILES_LINTED=1 with that same absolute path in it, so the run was not a green over zero files; (3) untouched files cannot move, because this repo's single eslint.config.mjs says of itself that it never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, so no verdict on a file this diff did not touch depends on this diff. Result: ESLINT_EXIT=0, ERRORS=0 WARNINGS=0.
  • CI itself. Reported at draft-PR time by contract; the gate jobs on this PR had not converged when this body was written.

Draft on purpose: scripts/pm/** is governed, so this stays draft and merges by hand; review requests are the PM seat's step. skip-changeset is applied — the diff is one self-test case in a script under scripts/pm/** and publishes nothing from any package, which is the shape scripts/check-empty-changeset.mjs enumerates for the label (It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like) -> ... apply the 'skip-changeset' label). The label was written as a union with what the PR already carried and read back afterwards: size/s, skip-changeset, nothing stripped.

Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

…e self-test
One case walks the two roots the promise covers — the pm-dispatch skill tree
and this tool's own directory, both DERIVED (the directory of the mandate glob
naming the skill's main file; this module's own directory) rather than spelled,
so the population follows a rename and no fresh path literal joins this file's
watch-hint set. It counts occurrences of the tier constant's own VALUE, read
from the constant at run time — never a second literal, never a family or
prefix pattern — and requires exactly one, at the definition line, reporting
every offending file:line when it is not.
Until now that promise was held by a git grep someone remembers to run; the
drift it exists against was sixteen spellings across these same two roots.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026 — with Claude
…wed import
`relative` from `node:path` is bound to a fixture STRING inside `selfTest()`,
a few thousand lines above the new case, so calling it there is a TypeError at
run time rather than a name error at parse time — measured: the battery died
on the case's first statement with `relative is not a function`, after 1240
green cases. The repo-relative spelling now comes from ROOT, which every
sibling case in this battery already joins against, and the own-source read
goes through `import.meta.url` directly. A ROOT prefix that stops holding
leaves an empty root, which the walk reports instead of turning into a pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616) - #14672

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin
Sep 2, 2026
Merged

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616)#14672
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14616

One check:pm-dispatch-gates self-test case, in the tier region of selfTest() in scripts/pm/dispatch-gates.mjs. Nothing else in the tree moves: the constant is untouched, .claude/skills/pm-dispatch/** is untouched, scripts/pm/check-skill-line-ratchet.mjs is untouched. One file, two commits (the second is the fix for a shadowed import the first battery found — see "What the first run measured" below).

The case

What it walks. Two roots, both DERIVED rather than spelled:

  • the pm-dispatch skill tree — dirname() of the mandate glob that names the skill's main file, taken from MANDATORY_TIER_GLOBS, a population this module already declares;
  • this tool's own directory — its own module path, taken relative to ROOT.

At this commit that resolves to .claude/skills/pm-dispatch + scripts/pm, 21+17 files. Derivation is not decoration: the population follows a rename instead of rotting into a scan that reaches nothing, and a mandate glob renamed away leaves an empty root, which the walk reports as a failure rather than quietly walking the whole tree.

What it counts. Occurrences of the tier constant's own VALUE, read from the constant at run time. Never a second literal — that spelling would itself BE the extra site the case exists to forbid — and never a family or prefix pattern: a prefix scan would be a family floor the human floor has not granted, and would itself be a second spelling of the model family inside this tool, i.e. the defect recreated by its own guard.

What it reports. Every occurrence as file:line, in the case name, next to the definition site it expected and the per-root file counts. A bare count is not actionable; what a reader gets on failure is the offending list. It is one t() call, non-vacuous by construction: it fails if either root scanned zero files, if the definition line cannot be found, if the count is not 1, or if the one site is not the definition line. The definition line is FOUND (the single line carrying both the constant's name and its value), never written down, so the case keeps no hand-maintained line number of its own.

Its green line, from the run below:

✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)

The two guards named in the filing, both measured

1. Watch-hint extraction and the declared population. This module's own extractWatchHints run over its own source, before and after the edit:

before (2aa8456c): 23 hints
after (da479d10): 23 hints — identical sets, element for element

The case adds no path literal to that set, for two independent reasons, and the belt-and-braces is deliberate: the roots are derived rather than spelled at all, and maskSelfTests blanks the selfTest() body before extraction, which is what makes any fixture path in that region inert in the first place. The module's inherited-population declaration is untouched and still true — declaredInheritedPopulation() over the edited source still answers .github/workflows and nothing else, so a follower inherits exactly what it inherited before.

The two gates that guard this class agree, each quoted from its own verdict line:

✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 29, ROOT_FILE_WATCH_HINTS 9, ROOT_WATCH_HINTS 2, DECLARED_WATCH_HINTS 5 -- every one an array of quoted literals inside its own statement, every rostered name non-empty, and no unrostered spelling of the idiom in the tree.
✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).

2. The verbatim maintainer quotes stay out of reach. The rulings quoted in the skill's main file and in references/dispatch-runbook.md name the model FAMILY as prose, not this constant's value, so an exact-value scan does not match them — measured rather than assumed: the scan over both roots returns exactly one site, and neither quote is in it. The case's own comment records this as a boundary to keep, not an accident to fix later: a gate that could demand edits to a maintainer's recorded words is the wrong gate, so the case must not be widened until it would reach them.

Red then green — predict-then-mutate

Prediction, written before the run: a SECOND spelling of the value anywhere under the two roots reds exactly this one case, and the failure names the offending path. No build leg to get wrong — a plain .mjs script, read from source.

The mutation is a file inside the population carrying the value, written from the constant at run time (the mutation script never spells it either); the restore is its removal, proved by observing state and not by any step's exit code. All three legs ran back to back in one hold of the container's shared verify lock, at da479d10:

=== LEG 1: green on the committed implementation ===
LEG1_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.
=== LEG 2: mutate — a SECOND spelling of the value inside the population ===
MUT_PRESENT=yes
MUT_VALUE_HITS=1
MUT_BYTES=23
LEG2_EXIT=1
✗ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+18 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955, scripts/pm/.tier-value-site-mutation.tmp:1)
✗ dispatch-gates self-test: 1 of 1241 case(s) failed.
=== LEG 3: restore, then MEASURE the restored tree ===
MUT_ABSENT=yes
GIT_STATUS_LINES=0
GIT_DIFF_HEAD_LINES=0
LEG3_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.

Four things that pair proves rather than asserts. The mutation reached disk before anything was read (MUT_VALUE_HITS=1, 23 bytes) — the leg was not a no-op reading healthy output. The file count moved 21+17 to 21+18, so the walk really opened it. Exactly ONE of 1241 cases failed, so the mutation reddened this case and nothing else. And the restore leg was MEASURED, not assumed: the file is gone, the tree is clean by git status and by git diff HEAD, and the full battery is green again on the restored tree.

Case count, which is the other half of "one case": 1240 cases pass at 2aa8456c before the edit, 1241 cases pass after — one case added, none replaced.

What the first run measured (kept, because it is the interesting part)

The first battery on this branch died on the new case's first statement:

TypeError: relative is not a function

relative from node:path is bound to a fixture STRING inside selfTest(), thousands of lines above the new case, so the shadowing is invisible at parse time and node --check passes. The second commit takes the repo-relative spelling from ROOT instead — the prefix every sibling case in this battery already joins against — and reads its own source through import.meta.url. A ROOT prefix that stops holding leaves an empty root, which the walk reports rather than turning into a pass. The rest of the scope was then swept for the same trap: relative and resolve are the only two node:path names shadowed anywhere in that function, and this case uses neither now.

Gates

Family re-derived AFTER the edit from the tool itself (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no hand-written path list), run one at a time under the shared verify lock, each exit code captured before any pipe. Head sha for every row: da479d10 (git rev-parse --short HEAD in that run; the tree was clean and no commit followed).

gateexitits own verdict line
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass. (legs 1 and 3)
pnpm check:watch-hint-literal0✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.
pnpm check:declared-population-live0✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).
pnpm check:entry-guard0✓ check:entry-guard: 199 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 150 export bindings, 150 of them inert on import (0 known-unsafe, SHRINK-ONLY).
pnpm check:parse-guard0✓ check:parse-guard: 198 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered by core or crosspkg …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
pnpm check:cross-package-test-inputs0same verdict line as the direct invocation above
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
pnpm check:agent-test-spelling0✓ check-agent-test-spelling: 0 violations — 425 file(s) · 5645 bare -- token(s) · 1380 launcher-rooted run(s) · 9 separator(s) JUDGED …
pnpm check:bash32-floor0✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment …
pnpm check:cli-command-ids0✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path …
pnpm check:pnpm-filter-targets0✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 32 file(s) resolve against 79 workspace package(s) …
node scripts/check-nul-bytes.mjs0check-nul-bytes: OK (scanned 7999 text file(s) … no raw ASCII control bytes).
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

check:nul-bytes is not in the derived family; it is run on every edit by standing rule, and the diff was separately swept for raw control bytes before the first commit.

NOT MEASURED

  • node scripts/check-test-completeness.mjs — exit 3, prerequisite not met. Its own text: check-test-completeness: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named, and, for exactly this situation, running the family locally, record this gate as NOT MEASURED. It is not a red, and there is nothing here to fix. Recorded as NOT MEASURED, not as a failure and not as a pass.
  • Repo-wide pnpm lint — narrowed deliberately, with the narrowing's own evidence. CI's Lint and Repo Gates runs the whole farm regardless; locally this ran targeted, and the three readings a narrowing owes are all here: (1) the population came from git, not from a guess — git diff --name-only 2aa8456c HEAD returns exactly one path, scripts/pm/dispatch-gates.mjs; (2) the file count came from eslint's own --format json output, FILES_LINTED=1 with that same absolute path in it, so the run was not a green over zero files; (3) untouched files cannot move, because this repo's single eslint.config.mjs says of itself that it never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, so no verdict on a file this diff did not touch depends on this diff. Result: ESLINT_EXIT=0, ERRORS=0 WARNINGS=0.
  • CI itself. Reported at draft-PR time by contract; the gate jobs on this PR had not converged when this body was written.

Draft on purpose: scripts/pm/** is governed, so this stays draft and merges by hand; review requests are the PM seat's step. skip-changeset is applied — the diff is one self-test case in a script under scripts/pm/** and publishes nothing from any package, which is the shape scripts/check-empty-changeset.mjs enumerates for the label (It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like) -> ... apply the 'skip-changeset' label). The label was written as a union with what the PR already carried and read back afterwards: size/s, skip-changeset, nothing stripped.

Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

…e self-test
One case walks the two roots the promise covers — the pm-dispatch skill tree
and this tool's own directory, both DERIVED (the directory of the mandate glob
naming the skill's main file; this module's own directory) rather than spelled,
so the population follows a rename and no fresh path literal joins this file's
watch-hint set. It counts occurrences of the tier constant's own VALUE, read
from the constant at run time — never a second literal, never a family or
prefix pattern — and requires exactly one, at the definition line, reporting
every offending file:line when it is not.
Until now that promise was held by a git grep someone remembers to run; the
drift it exists against was sixteen spellings across these same two roots.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026 — with Claude
…wed import
`relative` from `node:path` is bound to a fixture STRING inside `selfTest()`,
a few thousand lines above the new case, so calling it there is a TypeError at
run time rather than a name error at parse time — measured: the battery died
on the case's first statement with `relative is not a function`, after 1240
green cases. The repo-relative spelling now comes from ROOT, which every
sibling case in this battery already joins against, and the own-source read
goes through `import.meta.url` directly. A ROOT prefix that stops holding
leaves an empty root, which the walk reports instead of turning into a pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616) - #14672

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin
Sep 2, 2026
Merged

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616)#14672
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14616

One check:pm-dispatch-gates self-test case, in the tier region of selfTest() in scripts/pm/dispatch-gates.mjs. Nothing else in the tree moves: the constant is untouched, .claude/skills/pm-dispatch/** is untouched, scripts/pm/check-skill-line-ratchet.mjs is untouched. One file, two commits (the second is the fix for a shadowed import the first battery found — see "What the first run measured" below).

The case

What it walks. Two roots, both DERIVED rather than spelled:

  • the pm-dispatch skill tree — dirname() of the mandate glob that names the skill's main file, taken from MANDATORY_TIER_GLOBS, a population this module already declares;
  • this tool's own directory — its own module path, taken relative to ROOT.

At this commit that resolves to .claude/skills/pm-dispatch + scripts/pm, 21+17 files. Derivation is not decoration: the population follows a rename instead of rotting into a scan that reaches nothing, and a mandate glob renamed away leaves an empty root, which the walk reports as a failure rather than quietly walking the whole tree.

What it counts. Occurrences of the tier constant's own VALUE, read from the constant at run time. Never a second literal — that spelling would itself BE the extra site the case exists to forbid — and never a family or prefix pattern: a prefix scan would be a family floor the human floor has not granted, and would itself be a second spelling of the model family inside this tool, i.e. the defect recreated by its own guard.

What it reports. Every occurrence as file:line, in the case name, next to the definition site it expected and the per-root file counts. A bare count is not actionable; what a reader gets on failure is the offending list. It is one t() call, non-vacuous by construction: it fails if either root scanned zero files, if the definition line cannot be found, if the count is not 1, or if the one site is not the definition line. The definition line is FOUND (the single line carrying both the constant's name and its value), never written down, so the case keeps no hand-maintained line number of its own.

Its green line, from the run below:

✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)

The two guards named in the filing, both measured

1. Watch-hint extraction and the declared population. This module's own extractWatchHints run over its own source, before and after the edit:

before (2aa8456c): 23 hints
after (da479d10): 23 hints — identical sets, element for element

The case adds no path literal to that set, for two independent reasons, and the belt-and-braces is deliberate: the roots are derived rather than spelled at all, and maskSelfTests blanks the selfTest() body before extraction, which is what makes any fixture path in that region inert in the first place. The module's inherited-population declaration is untouched and still true — declaredInheritedPopulation() over the edited source still answers .github/workflows and nothing else, so a follower inherits exactly what it inherited before.

The two gates that guard this class agree, each quoted from its own verdict line:

✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 29, ROOT_FILE_WATCH_HINTS 9, ROOT_WATCH_HINTS 2, DECLARED_WATCH_HINTS 5 -- every one an array of quoted literals inside its own statement, every rostered name non-empty, and no unrostered spelling of the idiom in the tree.
✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).

2. The verbatim maintainer quotes stay out of reach. The rulings quoted in the skill's main file and in references/dispatch-runbook.md name the model FAMILY as prose, not this constant's value, so an exact-value scan does not match them — measured rather than assumed: the scan over both roots returns exactly one site, and neither quote is in it. The case's own comment records this as a boundary to keep, not an accident to fix later: a gate that could demand edits to a maintainer's recorded words is the wrong gate, so the case must not be widened until it would reach them.

Red then green — predict-then-mutate

Prediction, written before the run: a SECOND spelling of the value anywhere under the two roots reds exactly this one case, and the failure names the offending path. No build leg to get wrong — a plain .mjs script, read from source.

The mutation is a file inside the population carrying the value, written from the constant at run time (the mutation script never spells it either); the restore is its removal, proved by observing state and not by any step's exit code. All three legs ran back to back in one hold of the container's shared verify lock, at da479d10:

=== LEG 1: green on the committed implementation ===
LEG1_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.
=== LEG 2: mutate — a SECOND spelling of the value inside the population ===
MUT_PRESENT=yes
MUT_VALUE_HITS=1
MUT_BYTES=23
LEG2_EXIT=1
✗ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+18 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955, scripts/pm/.tier-value-site-mutation.tmp:1)
✗ dispatch-gates self-test: 1 of 1241 case(s) failed.
=== LEG 3: restore, then MEASURE the restored tree ===
MUT_ABSENT=yes
GIT_STATUS_LINES=0
GIT_DIFF_HEAD_LINES=0
LEG3_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.

Four things that pair proves rather than asserts. The mutation reached disk before anything was read (MUT_VALUE_HITS=1, 23 bytes) — the leg was not a no-op reading healthy output. The file count moved 21+17 to 21+18, so the walk really opened it. Exactly ONE of 1241 cases failed, so the mutation reddened this case and nothing else. And the restore leg was MEASURED, not assumed: the file is gone, the tree is clean by git status and by git diff HEAD, and the full battery is green again on the restored tree.

Case count, which is the other half of "one case": 1240 cases pass at 2aa8456c before the edit, 1241 cases pass after — one case added, none replaced.

What the first run measured (kept, because it is the interesting part)

The first battery on this branch died on the new case's first statement:

TypeError: relative is not a function

relative from node:path is bound to a fixture STRING inside selfTest(), thousands of lines above the new case, so the shadowing is invisible at parse time and node --check passes. The second commit takes the repo-relative spelling from ROOT instead — the prefix every sibling case in this battery already joins against — and reads its own source through import.meta.url. A ROOT prefix that stops holding leaves an empty root, which the walk reports rather than turning into a pass. The rest of the scope was then swept for the same trap: relative and resolve are the only two node:path names shadowed anywhere in that function, and this case uses neither now.

Gates

Family re-derived AFTER the edit from the tool itself (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no hand-written path list), run one at a time under the shared verify lock, each exit code captured before any pipe. Head sha for every row: da479d10 (git rev-parse --short HEAD in that run; the tree was clean and no commit followed).

gateexitits own verdict line
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass. (legs 1 and 3)
pnpm check:watch-hint-literal0✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.
pnpm check:declared-population-live0✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).
pnpm check:entry-guard0✓ check:entry-guard: 199 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 150 export bindings, 150 of them inert on import (0 known-unsafe, SHRINK-ONLY).
pnpm check:parse-guard0✓ check:parse-guard: 198 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered by core or crosspkg …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
pnpm check:cross-package-test-inputs0same verdict line as the direct invocation above
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
pnpm check:agent-test-spelling0✓ check-agent-test-spelling: 0 violations — 425 file(s) · 5645 bare -- token(s) · 1380 launcher-rooted run(s) · 9 separator(s) JUDGED …
pnpm check:bash32-floor0✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment …
pnpm check:cli-command-ids0✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path …
pnpm check:pnpm-filter-targets0✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 32 file(s) resolve against 79 workspace package(s) …
node scripts/check-nul-bytes.mjs0check-nul-bytes: OK (scanned 7999 text file(s) … no raw ASCII control bytes).
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

check:nul-bytes is not in the derived family; it is run on every edit by standing rule, and the diff was separately swept for raw control bytes before the first commit.

NOT MEASURED

  • node scripts/check-test-completeness.mjs — exit 3, prerequisite not met. Its own text: check-test-completeness: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named, and, for exactly this situation, running the family locally, record this gate as NOT MEASURED. It is not a red, and there is nothing here to fix. Recorded as NOT MEASURED, not as a failure and not as a pass.
  • Repo-wide pnpm lint — narrowed deliberately, with the narrowing's own evidence. CI's Lint and Repo Gates runs the whole farm regardless; locally this ran targeted, and the three readings a narrowing owes are all here: (1) the population came from git, not from a guess — git diff --name-only 2aa8456c HEAD returns exactly one path, scripts/pm/dispatch-gates.mjs; (2) the file count came from eslint's own --format json output, FILES_LINTED=1 with that same absolute path in it, so the run was not a green over zero files; (3) untouched files cannot move, because this repo's single eslint.config.mjs says of itself that it never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, so no verdict on a file this diff did not touch depends on this diff. Result: ESLINT_EXIT=0, ERRORS=0 WARNINGS=0.
  • CI itself. Reported at draft-PR time by contract; the gate jobs on this PR had not converged when this body was written.

Draft on purpose: scripts/pm/** is governed, so this stays draft and merges by hand; review requests are the PM seat's step. skip-changeset is applied — the diff is one self-test case in a script under scripts/pm/** and publishes nothing from any package, which is the shape scripts/check-empty-changeset.mjs enumerates for the label (It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like) -> ... apply the 'skip-changeset' label). The label was written as a union with what the PR already carried and read back afterwards: size/s, skip-changeset, nothing stripped.

Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

…e self-test
One case walks the two roots the promise covers — the pm-dispatch skill tree
and this tool's own directory, both DERIVED (the directory of the mandate glob
naming the skill's main file; this module's own directory) rather than spelled,
so the population follows a rename and no fresh path literal joins this file's
watch-hint set. It counts occurrences of the tier constant's own VALUE, read
from the constant at run time — never a second literal, never a family or
prefix pattern — and requires exactly one, at the definition line, reporting
every offending file:line when it is not.
Until now that promise was held by a git grep someone remembers to run; the
drift it exists against was sixteen spellings across these same two roots.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026 — with Claude
…wed import
`relative` from `node:path` is bound to a fixture STRING inside `selfTest()`,
a few thousand lines above the new case, so calling it there is a TypeError at
run time rather than a name error at parse time — measured: the battery died
on the case's first statement with `relative is not a function`, after 1240
green cases. The repo-relative spelling now comes from ROOT, which every
sibling case in this battery already joins against, and the own-source read
goes through `import.meta.url` directly. A ROOT prefix that stops holding
leaves an empty root, which the walk reports instead of turning into a pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616) - #14672

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin
Sep 2, 2026
Merged

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616)#14672
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14616

One check:pm-dispatch-gates self-test case, in the tier region of selfTest() in scripts/pm/dispatch-gates.mjs. Nothing else in the tree moves: the constant is untouched, .claude/skills/pm-dispatch/** is untouched, scripts/pm/check-skill-line-ratchet.mjs is untouched. One file, two commits (the second is the fix for a shadowed import the first battery found — see "What the first run measured" below).

The case

What it walks. Two roots, both DERIVED rather than spelled:

  • the pm-dispatch skill tree — dirname() of the mandate glob that names the skill's main file, taken from MANDATORY_TIER_GLOBS, a population this module already declares;
  • this tool's own directory — its own module path, taken relative to ROOT.

At this commit that resolves to .claude/skills/pm-dispatch + scripts/pm, 21+17 files. Derivation is not decoration: the population follows a rename instead of rotting into a scan that reaches nothing, and a mandate glob renamed away leaves an empty root, which the walk reports as a failure rather than quietly walking the whole tree.

What it counts. Occurrences of the tier constant's own VALUE, read from the constant at run time. Never a second literal — that spelling would itself BE the extra site the case exists to forbid — and never a family or prefix pattern: a prefix scan would be a family floor the human floor has not granted, and would itself be a second spelling of the model family inside this tool, i.e. the defect recreated by its own guard.

What it reports. Every occurrence as file:line, in the case name, next to the definition site it expected and the per-root file counts. A bare count is not actionable; what a reader gets on failure is the offending list. It is one t() call, non-vacuous by construction: it fails if either root scanned zero files, if the definition line cannot be found, if the count is not 1, or if the one site is not the definition line. The definition line is FOUND (the single line carrying both the constant's name and its value), never written down, so the case keeps no hand-maintained line number of its own.

Its green line, from the run below:

✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)

The two guards named in the filing, both measured

1. Watch-hint extraction and the declared population. This module's own extractWatchHints run over its own source, before and after the edit:

before (2aa8456c): 23 hints
after (da479d10): 23 hints — identical sets, element for element

The case adds no path literal to that set, for two independent reasons, and the belt-and-braces is deliberate: the roots are derived rather than spelled at all, and maskSelfTests blanks the selfTest() body before extraction, which is what makes any fixture path in that region inert in the first place. The module's inherited-population declaration is untouched and still true — declaredInheritedPopulation() over the edited source still answers .github/workflows and nothing else, so a follower inherits exactly what it inherited before.

The two gates that guard this class agree, each quoted from its own verdict line:

✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 29, ROOT_FILE_WATCH_HINTS 9, ROOT_WATCH_HINTS 2, DECLARED_WATCH_HINTS 5 -- every one an array of quoted literals inside its own statement, every rostered name non-empty, and no unrostered spelling of the idiom in the tree.
✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).

2. The verbatim maintainer quotes stay out of reach. The rulings quoted in the skill's main file and in references/dispatch-runbook.md name the model FAMILY as prose, not this constant's value, so an exact-value scan does not match them — measured rather than assumed: the scan over both roots returns exactly one site, and neither quote is in it. The case's own comment records this as a boundary to keep, not an accident to fix later: a gate that could demand edits to a maintainer's recorded words is the wrong gate, so the case must not be widened until it would reach them.

Red then green — predict-then-mutate

Prediction, written before the run: a SECOND spelling of the value anywhere under the two roots reds exactly this one case, and the failure names the offending path. No build leg to get wrong — a plain .mjs script, read from source.

The mutation is a file inside the population carrying the value, written from the constant at run time (the mutation script never spells it either); the restore is its removal, proved by observing state and not by any step's exit code. All three legs ran back to back in one hold of the container's shared verify lock, at da479d10:

=== LEG 1: green on the committed implementation ===
LEG1_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.
=== LEG 2: mutate — a SECOND spelling of the value inside the population ===
MUT_PRESENT=yes
MUT_VALUE_HITS=1
MUT_BYTES=23
LEG2_EXIT=1
✗ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+18 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955, scripts/pm/.tier-value-site-mutation.tmp:1)
✗ dispatch-gates self-test: 1 of 1241 case(s) failed.
=== LEG 3: restore, then MEASURE the restored tree ===
MUT_ABSENT=yes
GIT_STATUS_LINES=0
GIT_DIFF_HEAD_LINES=0
LEG3_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.

Four things that pair proves rather than asserts. The mutation reached disk before anything was read (MUT_VALUE_HITS=1, 23 bytes) — the leg was not a no-op reading healthy output. The file count moved 21+17 to 21+18, so the walk really opened it. Exactly ONE of 1241 cases failed, so the mutation reddened this case and nothing else. And the restore leg was MEASURED, not assumed: the file is gone, the tree is clean by git status and by git diff HEAD, and the full battery is green again on the restored tree.

Case count, which is the other half of "one case": 1240 cases pass at 2aa8456c before the edit, 1241 cases pass after — one case added, none replaced.

What the first run measured (kept, because it is the interesting part)

The first battery on this branch died on the new case's first statement:

TypeError: relative is not a function

relative from node:path is bound to a fixture STRING inside selfTest(), thousands of lines above the new case, so the shadowing is invisible at parse time and node --check passes. The second commit takes the repo-relative spelling from ROOT instead — the prefix every sibling case in this battery already joins against — and reads its own source through import.meta.url. A ROOT prefix that stops holding leaves an empty root, which the walk reports rather than turning into a pass. The rest of the scope was then swept for the same trap: relative and resolve are the only two node:path names shadowed anywhere in that function, and this case uses neither now.

Gates

Family re-derived AFTER the edit from the tool itself (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no hand-written path list), run one at a time under the shared verify lock, each exit code captured before any pipe. Head sha for every row: da479d10 (git rev-parse --short HEAD in that run; the tree was clean and no commit followed).

gateexitits own verdict line
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass. (legs 1 and 3)
pnpm check:watch-hint-literal0✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.
pnpm check:declared-population-live0✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).
pnpm check:entry-guard0✓ check:entry-guard: 199 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 150 export bindings, 150 of them inert on import (0 known-unsafe, SHRINK-ONLY).
pnpm check:parse-guard0✓ check:parse-guard: 198 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered by core or crosspkg …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
pnpm check:cross-package-test-inputs0same verdict line as the direct invocation above
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
pnpm check:agent-test-spelling0✓ check-agent-test-spelling: 0 violations — 425 file(s) · 5645 bare -- token(s) · 1380 launcher-rooted run(s) · 9 separator(s) JUDGED …
pnpm check:bash32-floor0✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment …
pnpm check:cli-command-ids0✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path …
pnpm check:pnpm-filter-targets0✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 32 file(s) resolve against 79 workspace package(s) …
node scripts/check-nul-bytes.mjs0check-nul-bytes: OK (scanned 7999 text file(s) … no raw ASCII control bytes).
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

check:nul-bytes is not in the derived family; it is run on every edit by standing rule, and the diff was separately swept for raw control bytes before the first commit.

NOT MEASURED

  • node scripts/check-test-completeness.mjs — exit 3, prerequisite not met. Its own text: check-test-completeness: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named, and, for exactly this situation, running the family locally, record this gate as NOT MEASURED. It is not a red, and there is nothing here to fix. Recorded as NOT MEASURED, not as a failure and not as a pass.
  • Repo-wide pnpm lint — narrowed deliberately, with the narrowing's own evidence. CI's Lint and Repo Gates runs the whole farm regardless; locally this ran targeted, and the three readings a narrowing owes are all here: (1) the population came from git, not from a guess — git diff --name-only 2aa8456c HEAD returns exactly one path, scripts/pm/dispatch-gates.mjs; (2) the file count came from eslint's own --format json output, FILES_LINTED=1 with that same absolute path in it, so the run was not a green over zero files; (3) untouched files cannot move, because this repo's single eslint.config.mjs says of itself that it never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, so no verdict on a file this diff did not touch depends on this diff. Result: ESLINT_EXIT=0, ERRORS=0 WARNINGS=0.
  • CI itself. Reported at draft-PR time by contract; the gate jobs on this PR had not converged when this body was written.

Draft on purpose: scripts/pm/** is governed, so this stays draft and merges by hand; review requests are the PM seat's step. skip-changeset is applied — the diff is one self-test case in a script under scripts/pm/** and publishes nothing from any package, which is the shape scripts/check-empty-changeset.mjs enumerates for the label (It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like) -> ... apply the 'skip-changeset' label). The label was written as a union with what the PR already carried and read back afterwards: size/s, skip-changeset, nothing stripped.

Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

…e self-test
One case walks the two roots the promise covers — the pm-dispatch skill tree
and this tool's own directory, both DERIVED (the directory of the mandate glob
naming the skill's main file; this module's own directory) rather than spelled,
so the population follows a rename and no fresh path literal joins this file's
watch-hint set. It counts occurrences of the tier constant's own VALUE, read
from the constant at run time — never a second literal, never a family or
prefix pattern — and requires exactly one, at the definition line, reporting
every offending file:line when it is not.
Until now that promise was held by a git grep someone remembers to run; the
drift it exists against was sixteen spellings across these same two roots.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026 — with Claude
…wed import
`relative` from `node:path` is bound to a fixture STRING inside `selfTest()`,
a few thousand lines above the new case, so calling it there is a TypeError at
run time rather than a name error at parse time — measured: the battery died
on the case's first statement with `relative is not a function`, after 1240
green cases. The repo-relative spelling now comes from ROOT, which every
sibling case in this battery already joins against, and the own-source read
goes through `import.meta.url` directly. A ROOT prefix that stops holding
leaves an empty root, which the walk reports instead of turning into a pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616) - #14672

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin
Sep 2, 2026
Merged

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616)#14672
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14616

One check:pm-dispatch-gates self-test case, in the tier region of selfTest() in scripts/pm/dispatch-gates.mjs. Nothing else in the tree moves: the constant is untouched, .claude/skills/pm-dispatch/** is untouched, scripts/pm/check-skill-line-ratchet.mjs is untouched. One file, two commits (the second is the fix for a shadowed import the first battery found — see "What the first run measured" below).

The case

What it walks. Two roots, both DERIVED rather than spelled:

  • the pm-dispatch skill tree — dirname() of the mandate glob that names the skill's main file, taken from MANDATORY_TIER_GLOBS, a population this module already declares;
  • this tool's own directory — its own module path, taken relative to ROOT.

At this commit that resolves to .claude/skills/pm-dispatch + scripts/pm, 21+17 files. Derivation is not decoration: the population follows a rename instead of rotting into a scan that reaches nothing, and a mandate glob renamed away leaves an empty root, which the walk reports as a failure rather than quietly walking the whole tree.

What it counts. Occurrences of the tier constant's own VALUE, read from the constant at run time. Never a second literal — that spelling would itself BE the extra site the case exists to forbid — and never a family or prefix pattern: a prefix scan would be a family floor the human floor has not granted, and would itself be a second spelling of the model family inside this tool, i.e. the defect recreated by its own guard.

What it reports. Every occurrence as file:line, in the case name, next to the definition site it expected and the per-root file counts. A bare count is not actionable; what a reader gets on failure is the offending list. It is one t() call, non-vacuous by construction: it fails if either root scanned zero files, if the definition line cannot be found, if the count is not 1, or if the one site is not the definition line. The definition line is FOUND (the single line carrying both the constant's name and its value), never written down, so the case keeps no hand-maintained line number of its own.

Its green line, from the run below:

✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)

The two guards named in the filing, both measured

1. Watch-hint extraction and the declared population. This module's own extractWatchHints run over its own source, before and after the edit:

before (2aa8456c): 23 hints
after (da479d10): 23 hints — identical sets, element for element

The case adds no path literal to that set, for two independent reasons, and the belt-and-braces is deliberate: the roots are derived rather than spelled at all, and maskSelfTests blanks the selfTest() body before extraction, which is what makes any fixture path in that region inert in the first place. The module's inherited-population declaration is untouched and still true — declaredInheritedPopulation() over the edited source still answers .github/workflows and nothing else, so a follower inherits exactly what it inherited before.

The two gates that guard this class agree, each quoted from its own verdict line:

✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 29, ROOT_FILE_WATCH_HINTS 9, ROOT_WATCH_HINTS 2, DECLARED_WATCH_HINTS 5 -- every one an array of quoted literals inside its own statement, every rostered name non-empty, and no unrostered spelling of the idiom in the tree.
✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).

2. The verbatim maintainer quotes stay out of reach. The rulings quoted in the skill's main file and in references/dispatch-runbook.md name the model FAMILY as prose, not this constant's value, so an exact-value scan does not match them — measured rather than assumed: the scan over both roots returns exactly one site, and neither quote is in it. The case's own comment records this as a boundary to keep, not an accident to fix later: a gate that could demand edits to a maintainer's recorded words is the wrong gate, so the case must not be widened until it would reach them.

Red then green — predict-then-mutate

Prediction, written before the run: a SECOND spelling of the value anywhere under the two roots reds exactly this one case, and the failure names the offending path. No build leg to get wrong — a plain .mjs script, read from source.

The mutation is a file inside the population carrying the value, written from the constant at run time (the mutation script never spells it either); the restore is its removal, proved by observing state and not by any step's exit code. All three legs ran back to back in one hold of the container's shared verify lock, at da479d10:

=== LEG 1: green on the committed implementation ===
LEG1_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.
=== LEG 2: mutate — a SECOND spelling of the value inside the population ===
MUT_PRESENT=yes
MUT_VALUE_HITS=1
MUT_BYTES=23
LEG2_EXIT=1
✗ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+18 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955, scripts/pm/.tier-value-site-mutation.tmp:1)
✗ dispatch-gates self-test: 1 of 1241 case(s) failed.
=== LEG 3: restore, then MEASURE the restored tree ===
MUT_ABSENT=yes
GIT_STATUS_LINES=0
GIT_DIFF_HEAD_LINES=0
LEG3_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.

Four things that pair proves rather than asserts. The mutation reached disk before anything was read (MUT_VALUE_HITS=1, 23 bytes) — the leg was not a no-op reading healthy output. The file count moved 21+17 to 21+18, so the walk really opened it. Exactly ONE of 1241 cases failed, so the mutation reddened this case and nothing else. And the restore leg was MEASURED, not assumed: the file is gone, the tree is clean by git status and by git diff HEAD, and the full battery is green again on the restored tree.

Case count, which is the other half of "one case": 1240 cases pass at 2aa8456c before the edit, 1241 cases pass after — one case added, none replaced.

What the first run measured (kept, because it is the interesting part)

The first battery on this branch died on the new case's first statement:

TypeError: relative is not a function

relative from node:path is bound to a fixture STRING inside selfTest(), thousands of lines above the new case, so the shadowing is invisible at parse time and node --check passes. The second commit takes the repo-relative spelling from ROOT instead — the prefix every sibling case in this battery already joins against — and reads its own source through import.meta.url. A ROOT prefix that stops holding leaves an empty root, which the walk reports rather than turning into a pass. The rest of the scope was then swept for the same trap: relative and resolve are the only two node:path names shadowed anywhere in that function, and this case uses neither now.

Gates

Family re-derived AFTER the edit from the tool itself (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no hand-written path list), run one at a time under the shared verify lock, each exit code captured before any pipe. Head sha for every row: da479d10 (git rev-parse --short HEAD in that run; the tree was clean and no commit followed).

gateexitits own verdict line
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass. (legs 1 and 3)
pnpm check:watch-hint-literal0✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.
pnpm check:declared-population-live0✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).
pnpm check:entry-guard0✓ check:entry-guard: 199 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 150 export bindings, 150 of them inert on import (0 known-unsafe, SHRINK-ONLY).
pnpm check:parse-guard0✓ check:parse-guard: 198 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered by core or crosspkg …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
pnpm check:cross-package-test-inputs0same verdict line as the direct invocation above
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
pnpm check:agent-test-spelling0✓ check-agent-test-spelling: 0 violations — 425 file(s) · 5645 bare -- token(s) · 1380 launcher-rooted run(s) · 9 separator(s) JUDGED …
pnpm check:bash32-floor0✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment …
pnpm check:cli-command-ids0✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path …
pnpm check:pnpm-filter-targets0✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 32 file(s) resolve against 79 workspace package(s) …
node scripts/check-nul-bytes.mjs0check-nul-bytes: OK (scanned 7999 text file(s) … no raw ASCII control bytes).
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

check:nul-bytes is not in the derived family; it is run on every edit by standing rule, and the diff was separately swept for raw control bytes before the first commit.

NOT MEASURED

  • node scripts/check-test-completeness.mjs — exit 3, prerequisite not met. Its own text: check-test-completeness: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named, and, for exactly this situation, running the family locally, record this gate as NOT MEASURED. It is not a red, and there is nothing here to fix. Recorded as NOT MEASURED, not as a failure and not as a pass.
  • Repo-wide pnpm lint — narrowed deliberately, with the narrowing's own evidence. CI's Lint and Repo Gates runs the whole farm regardless; locally this ran targeted, and the three readings a narrowing owes are all here: (1) the population came from git, not from a guess — git diff --name-only 2aa8456c HEAD returns exactly one path, scripts/pm/dispatch-gates.mjs; (2) the file count came from eslint's own --format json output, FILES_LINTED=1 with that same absolute path in it, so the run was not a green over zero files; (3) untouched files cannot move, because this repo's single eslint.config.mjs says of itself that it never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, so no verdict on a file this diff did not touch depends on this diff. Result: ESLINT_EXIT=0, ERRORS=0 WARNINGS=0.
  • CI itself. Reported at draft-PR time by contract; the gate jobs on this PR had not converged when this body was written.

Draft on purpose: scripts/pm/** is governed, so this stays draft and merges by hand; review requests are the PM seat's step. skip-changeset is applied — the diff is one self-test case in a script under scripts/pm/** and publishes nothing from any package, which is the shape scripts/check-empty-changeset.mjs enumerates for the label (It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like) -> ... apply the 'skip-changeset' label). The label was written as a union with what the PR already carried and read back afterwards: size/s, skip-changeset, nothing stripped.

Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

…e self-test
One case walks the two roots the promise covers — the pm-dispatch skill tree
and this tool's own directory, both DERIVED (the directory of the mandate glob
naming the skill's main file; this module's own directory) rather than spelled,
so the population follows a rename and no fresh path literal joins this file's
watch-hint set. It counts occurrences of the tier constant's own VALUE, read
from the constant at run time — never a second literal, never a family or
prefix pattern — and requires exactly one, at the definition line, reporting
every offending file:line when it is not.
Until now that promise was held by a git grep someone remembers to run; the
drift it exists against was sixteen spellings across these same two roots.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026 — with Claude
…wed import
`relative` from `node:path` is bound to a fixture STRING inside `selfTest()`,
a few thousand lines above the new case, so calling it there is a TypeError at
run time rather than a name error at parse time — measured: the battery died
on the case's first statement with `relative is not a function`, after 1240
green cases. The repo-relative spelling now comes from ROOT, which every
sibling case in this battery already joins against, and the own-source read
goes through `import.meta.url` directly. A ROOT prefix that stops holding
leaves an empty root, which the walk reports instead of turning into a pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616) - #14672

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin
Sep 2, 2026
Merged

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616)#14672
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14616

One check:pm-dispatch-gates self-test case, in the tier region of selfTest() in scripts/pm/dispatch-gates.mjs. Nothing else in the tree moves: the constant is untouched, .claude/skills/pm-dispatch/** is untouched, scripts/pm/check-skill-line-ratchet.mjs is untouched. One file, two commits (the second is the fix for a shadowed import the first battery found — see "What the first run measured" below).

The case

What it walks. Two roots, both DERIVED rather than spelled:

  • the pm-dispatch skill tree — dirname() of the mandate glob that names the skill's main file, taken from MANDATORY_TIER_GLOBS, a population this module already declares;
  • this tool's own directory — its own module path, taken relative to ROOT.

At this commit that resolves to .claude/skills/pm-dispatch + scripts/pm, 21+17 files. Derivation is not decoration: the population follows a rename instead of rotting into a scan that reaches nothing, and a mandate glob renamed away leaves an empty root, which the walk reports as a failure rather than quietly walking the whole tree.

What it counts. Occurrences of the tier constant's own VALUE, read from the constant at run time. Never a second literal — that spelling would itself BE the extra site the case exists to forbid — and never a family or prefix pattern: a prefix scan would be a family floor the human floor has not granted, and would itself be a second spelling of the model family inside this tool, i.e. the defect recreated by its own guard.

What it reports. Every occurrence as file:line, in the case name, next to the definition site it expected and the per-root file counts. A bare count is not actionable; what a reader gets on failure is the offending list. It is one t() call, non-vacuous by construction: it fails if either root scanned zero files, if the definition line cannot be found, if the count is not 1, or if the one site is not the definition line. The definition line is FOUND (the single line carrying both the constant's name and its value), never written down, so the case keeps no hand-maintained line number of its own.

Its green line, from the run below:

✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)

The two guards named in the filing, both measured

1. Watch-hint extraction and the declared population. This module's own extractWatchHints run over its own source, before and after the edit:

before (2aa8456c): 23 hints
after (da479d10): 23 hints — identical sets, element for element

The case adds no path literal to that set, for two independent reasons, and the belt-and-braces is deliberate: the roots are derived rather than spelled at all, and maskSelfTests blanks the selfTest() body before extraction, which is what makes any fixture path in that region inert in the first place. The module's inherited-population declaration is untouched and still true — declaredInheritedPopulation() over the edited source still answers .github/workflows and nothing else, so a follower inherits exactly what it inherited before.

The two gates that guard this class agree, each quoted from its own verdict line:

✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 29, ROOT_FILE_WATCH_HINTS 9, ROOT_WATCH_HINTS 2, DECLARED_WATCH_HINTS 5 -- every one an array of quoted literals inside its own statement, every rostered name non-empty, and no unrostered spelling of the idiom in the tree.
✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).

2. The verbatim maintainer quotes stay out of reach. The rulings quoted in the skill's main file and in references/dispatch-runbook.md name the model FAMILY as prose, not this constant's value, so an exact-value scan does not match them — measured rather than assumed: the scan over both roots returns exactly one site, and neither quote is in it. The case's own comment records this as a boundary to keep, not an accident to fix later: a gate that could demand edits to a maintainer's recorded words is the wrong gate, so the case must not be widened until it would reach them.

Red then green — predict-then-mutate

Prediction, written before the run: a SECOND spelling of the value anywhere under the two roots reds exactly this one case, and the failure names the offending path. No build leg to get wrong — a plain .mjs script, read from source.

The mutation is a file inside the population carrying the value, written from the constant at run time (the mutation script never spells it either); the restore is its removal, proved by observing state and not by any step's exit code. All three legs ran back to back in one hold of the container's shared verify lock, at da479d10:

=== LEG 1: green on the committed implementation ===
LEG1_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.
=== LEG 2: mutate — a SECOND spelling of the value inside the population ===
MUT_PRESENT=yes
MUT_VALUE_HITS=1
MUT_BYTES=23
LEG2_EXIT=1
✗ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+18 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955, scripts/pm/.tier-value-site-mutation.tmp:1)
✗ dispatch-gates self-test: 1 of 1241 case(s) failed.
=== LEG 3: restore, then MEASURE the restored tree ===
MUT_ABSENT=yes
GIT_STATUS_LINES=0
GIT_DIFF_HEAD_LINES=0
LEG3_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.

Four things that pair proves rather than asserts. The mutation reached disk before anything was read (MUT_VALUE_HITS=1, 23 bytes) — the leg was not a no-op reading healthy output. The file count moved 21+17 to 21+18, so the walk really opened it. Exactly ONE of 1241 cases failed, so the mutation reddened this case and nothing else. And the restore leg was MEASURED, not assumed: the file is gone, the tree is clean by git status and by git diff HEAD, and the full battery is green again on the restored tree.

Case count, which is the other half of "one case": 1240 cases pass at 2aa8456c before the edit, 1241 cases pass after — one case added, none replaced.

What the first run measured (kept, because it is the interesting part)

The first battery on this branch died on the new case's first statement:

TypeError: relative is not a function

relative from node:path is bound to a fixture STRING inside selfTest(), thousands of lines above the new case, so the shadowing is invisible at parse time and node --check passes. The second commit takes the repo-relative spelling from ROOT instead — the prefix every sibling case in this battery already joins against — and reads its own source through import.meta.url. A ROOT prefix that stops holding leaves an empty root, which the walk reports rather than turning into a pass. The rest of the scope was then swept for the same trap: relative and resolve are the only two node:path names shadowed anywhere in that function, and this case uses neither now.

Gates

Family re-derived AFTER the edit from the tool itself (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no hand-written path list), run one at a time under the shared verify lock, each exit code captured before any pipe. Head sha for every row: da479d10 (git rev-parse --short HEAD in that run; the tree was clean and no commit followed).

gateexitits own verdict line
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass. (legs 1 and 3)
pnpm check:watch-hint-literal0✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.
pnpm check:declared-population-live0✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).
pnpm check:entry-guard0✓ check:entry-guard: 199 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 150 export bindings, 150 of them inert on import (0 known-unsafe, SHRINK-ONLY).
pnpm check:parse-guard0✓ check:parse-guard: 198 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered by core or crosspkg …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
pnpm check:cross-package-test-inputs0same verdict line as the direct invocation above
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
pnpm check:agent-test-spelling0✓ check-agent-test-spelling: 0 violations — 425 file(s) · 5645 bare -- token(s) · 1380 launcher-rooted run(s) · 9 separator(s) JUDGED …
pnpm check:bash32-floor0✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment …
pnpm check:cli-command-ids0✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path …
pnpm check:pnpm-filter-targets0✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 32 file(s) resolve against 79 workspace package(s) …
node scripts/check-nul-bytes.mjs0check-nul-bytes: OK (scanned 7999 text file(s) … no raw ASCII control bytes).
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

check:nul-bytes is not in the derived family; it is run on every edit by standing rule, and the diff was separately swept for raw control bytes before the first commit.

NOT MEASURED

  • node scripts/check-test-completeness.mjs — exit 3, prerequisite not met. Its own text: check-test-completeness: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named, and, for exactly this situation, running the family locally, record this gate as NOT MEASURED. It is not a red, and there is nothing here to fix. Recorded as NOT MEASURED, not as a failure and not as a pass.
  • Repo-wide pnpm lint — narrowed deliberately, with the narrowing's own evidence. CI's Lint and Repo Gates runs the whole farm regardless; locally this ran targeted, and the three readings a narrowing owes are all here: (1) the population came from git, not from a guess — git diff --name-only 2aa8456c HEAD returns exactly one path, scripts/pm/dispatch-gates.mjs; (2) the file count came from eslint's own --format json output, FILES_LINTED=1 with that same absolute path in it, so the run was not a green over zero files; (3) untouched files cannot move, because this repo's single eslint.config.mjs says of itself that it never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, so no verdict on a file this diff did not touch depends on this diff. Result: ESLINT_EXIT=0, ERRORS=0 WARNINGS=0.
  • CI itself. Reported at draft-PR time by contract; the gate jobs on this PR had not converged when this body was written.

Draft on purpose: scripts/pm/** is governed, so this stays draft and merges by hand; review requests are the PM seat's step. skip-changeset is applied — the diff is one self-test case in a script under scripts/pm/** and publishes nothing from any package, which is the shape scripts/check-empty-changeset.mjs enumerates for the label (It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like) -> ... apply the 'skip-changeset' label). The label was written as a union with what the PR already carried and read back afterwards: size/s, skip-changeset, nothing stripped.

Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

…e self-test
One case walks the two roots the promise covers — the pm-dispatch skill tree
and this tool's own directory, both DERIVED (the directory of the mandate glob
naming the skill's main file; this module's own directory) rather than spelled,
so the population follows a rename and no fresh path literal joins this file's
watch-hint set. It counts occurrences of the tier constant's own VALUE, read
from the constant at run time — never a second literal, never a family or
prefix pattern — and requires exactly one, at the definition line, reporting
every offending file:line when it is not.
Until now that promise was held by a git grep someone remembers to run; the
drift it exists against was sixteen spellings across these same two roots.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026 — with Claude
…wed import
`relative` from `node:path` is bound to a fixture STRING inside `selfTest()`,
a few thousand lines above the new case, so calling it there is a TypeError at
run time rather than a name error at parse time — measured: the battery died
on the case's first statement with `relative is not a function`, after 1240
green cases. The repo-relative spelling now comes from ROOT, which every
sibling case in this battery already joins against, and the own-source read
goes through `import.meta.url` directly. A ROOT prefix that stops holding
leaves an empty root, which the walk reports instead of turning into a pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants

@os-litant@os-zhuang@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616) - #14672

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin
Sep 2, 2026
Merged

test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616)#14672
os-zhuang merged 2 commits into
mainfrom
claude/issue-14616-tier-single-value-site-pin

Conversation

@os-litant

@os-litantos-litant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14616

One check:pm-dispatch-gates self-test case, in the tier region of selfTest() in scripts/pm/dispatch-gates.mjs. Nothing else in the tree moves: the constant is untouched, .claude/skills/pm-dispatch/** is untouched, scripts/pm/check-skill-line-ratchet.mjs is untouched. One file, two commits (the second is the fix for a shadowed import the first battery found — see "What the first run measured" below).

The case

What it walks. Two roots, both DERIVED rather than spelled:

  • the pm-dispatch skill tree — dirname() of the mandate glob that names the skill's main file, taken from MANDATORY_TIER_GLOBS, a population this module already declares;
  • this tool's own directory — its own module path, taken relative to ROOT.

At this commit that resolves to .claude/skills/pm-dispatch + scripts/pm, 21+17 files. Derivation is not decoration: the population follows a rename instead of rotting into a scan that reaches nothing, and a mandate glob renamed away leaves an empty root, which the walk reports as a failure rather than quietly walking the whole tree.

What it counts. Occurrences of the tier constant's own VALUE, read from the constant at run time. Never a second literal — that spelling would itself BE the extra site the case exists to forbid — and never a family or prefix pattern: a prefix scan would be a family floor the human floor has not granted, and would itself be a second spelling of the model family inside this tool, i.e. the defect recreated by its own guard.

What it reports. Every occurrence as file:line, in the case name, next to the definition site it expected and the per-root file counts. A bare count is not actionable; what a reader gets on failure is the offending list. It is one t() call, non-vacuous by construction: it fails if either root scanned zero files, if the definition line cannot be found, if the count is not 1, or if the one site is not the definition line. The definition line is FOUND (the single line carrying both the constant's name and its value), never written down, so the case keeps no hand-maintained line number of its own.

Its green line, from the run below:

✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)

The two guards named in the filing, both measured

1. Watch-hint extraction and the declared population. This module's own extractWatchHints run over its own source, before and after the edit:

before (2aa8456c): 23 hints
after (da479d10): 23 hints — identical sets, element for element

The case adds no path literal to that set, for two independent reasons, and the belt-and-braces is deliberate: the roots are derived rather than spelled at all, and maskSelfTests blanks the selfTest() body before extraction, which is what makes any fixture path in that region inert in the first place. The module's inherited-population declaration is untouched and still true — declaredInheritedPopulation() over the edited source still answers .github/workflows and nothing else, so a follower inherits exactly what it inherited before.

The two gates that guard this class agree, each quoted from its own verdict line:

✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 29, ROOT_FILE_WATCH_HINTS 9, ROOT_WATCH_HINTS 2, DECLARED_WATCH_HINTS 5 -- every one an array of quoted literals inside its own statement, every rostered name non-empty, and no unrostered spelling of the idiom in the tree.
✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).

2. The verbatim maintainer quotes stay out of reach. The rulings quoted in the skill's main file and in references/dispatch-runbook.md name the model FAMILY as prose, not this constant's value, so an exact-value scan does not match them — measured rather than assumed: the scan over both roots returns exactly one site, and neither quote is in it. The case's own comment records this as a boundary to keep, not an accident to fix later: a gate that could demand edits to a maintainer's recorded words is the wrong gate, so the case must not be widened until it would reach them.

Red then green — predict-then-mutate

Prediction, written before the run: a SECOND spelling of the value anywhere under the two roots reds exactly this one case, and the failure names the offending path. No build leg to get wrong — a plain .mjs script, read from source.

The mutation is a file inside the population carrying the value, written from the constant at run time (the mutation script never spells it either); the restore is its removal, proved by observing state and not by any step's exit code. All three legs ran back to back in one hold of the container's shared verify lock, at da479d10:

=== LEG 1: green on the committed implementation ===
LEG1_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.
=== LEG 2: mutate — a SECOND spelling of the value inside the population ===
MUT_PRESENT=yes
MUT_VALUE_HITS=1
MUT_BYTES=23
LEG2_EXIT=1
✗ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+18 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955, scripts/pm/.tier-value-site-mutation.tmp:1)
✗ dispatch-gates self-test: 1 of 1241 case(s) failed.
=== LEG 3: restore, then MEASURE the restored tree ===
MUT_ABSENT=yes
GIT_STATUS_LINES=0
GIT_DIFF_HEAD_LINES=0
LEG3_EXIT=0
✓ the tier constant's VALUE is spelled in exactly ONE site under .claude/skills/pm-dispatch + scripts/pm — 21+17 files read, the definition at scripts/pm/dispatch-gates.mjs:7955 the only one allowed (found: scripts/pm/dispatch-gates.mjs:7955)
✓ dispatch-gates self-test: 1241 cases pass.

Four things that pair proves rather than asserts. The mutation reached disk before anything was read (MUT_VALUE_HITS=1, 23 bytes) — the leg was not a no-op reading healthy output. The file count moved 21+17 to 21+18, so the walk really opened it. Exactly ONE of 1241 cases failed, so the mutation reddened this case and nothing else. And the restore leg was MEASURED, not assumed: the file is gone, the tree is clean by git status and by git diff HEAD, and the full battery is green again on the restored tree.

Case count, which is the other half of "one case": 1240 cases pass at 2aa8456c before the edit, 1241 cases pass after — one case added, none replaced.

What the first run measured (kept, because it is the interesting part)

The first battery on this branch died on the new case's first statement:

TypeError: relative is not a function

relative from node:path is bound to a fixture STRING inside selfTest(), thousands of lines above the new case, so the shadowing is invisible at parse time and node --check passes. The second commit takes the repo-relative spelling from ROOT instead — the prefix every sibling case in this battery already joins against — and reads its own source through import.meta.url. A ROOT prefix that stops holding leaves an empty root, which the walk reports rather than turning into a pass. The rest of the scope was then swept for the same trap: relative and resolve are the only two node:path names shadowed anywhere in that function, and this case uses neither now.

Gates

Family re-derived AFTER the edit from the tool itself (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no hand-written path list), run one at a time under the shared verify lock, each exit code captured before any pipe. Head sha for every row: da479d10 (git rev-parse --short HEAD in that run; the tree was clean and no commit followed).

gateexitits own verdict line
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 1241 cases pass. (legs 1 and 3)
pnpm check:watch-hint-literal0✓ check-watch-hint-literal: 45 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.
pnpm check:declared-population-live0✓ check:declared-population-live — 158 of 202 famil(ies) declare a path population, and every one of them reaches this tree's 8006 tracked file(s).
pnpm check:entry-guard0✓ check:entry-guard: 199 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 150 export bindings, 150 of them inert on import (0 known-unsafe, SHRINK-ONLY).
pnpm check:parse-guard0✓ check:parse-guard: 198 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
node scripts/check-self-test-wired.mjs0✓ check-self-test-wired: every one of the 164 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-ci-filter-parity.mjs0OK: all 130 declared cross-package glob(s) (92 unique) are covered by core or crosspkg …
node scripts/check-cross-package-test-inputs.mjs0OK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
pnpm check:cross-package-test-inputs0same verdict line as the direct invocation above
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
pnpm check:agent-test-spelling0✓ check-agent-test-spelling: 0 violations — 425 file(s) · 5645 bare -- token(s) · 1380 launcher-rooted run(s) · 9 separator(s) JUDGED …
pnpm check:bash32-floor0✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment …
pnpm check:cli-command-ids0✓ check-cli-command-ids: 315 command-id literal(s) across 112 file(s) outside packages/cli all resolve to a real command path …
pnpm check:pnpm-filter-targets0✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 32 file(s) resolve against 79 workspace package(s) …
node scripts/check-nul-bytes.mjs0check-nul-bytes: OK (scanned 7999 text file(s) … no raw ASCII control bytes).
node scripts/check-test-completeness.mjs3NOT MEASURED — see below

check:nul-bytes is not in the derived family; it is run on every edit by standing rule, and the diff was separately swept for raw control bytes before the first commit.

NOT MEASURED

  • node scripts/check-test-completeness.mjs — exit 3, prerequisite not met. Its own text: check-test-completeness: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named, and, for exactly this situation, running the family locally, record this gate as NOT MEASURED. It is not a red, and there is nothing here to fix. Recorded as NOT MEASURED, not as a failure and not as a pass.
  • Repo-wide pnpm lint — narrowed deliberately, with the narrowing's own evidence. CI's Lint and Repo Gates runs the whole farm regardless; locally this ran targeted, and the three readings a narrowing owes are all here: (1) the population came from git, not from a guess — git diff --name-only 2aa8456c HEAD returns exactly one path, scripts/pm/dispatch-gates.mjs; (2) the file count came from eslint's own --format json output, FILES_LINTED=1 with that same absolute path in it, so the run was not a green over zero files; (3) untouched files cannot move, because this repo's single eslint.config.mjs says of itself that it never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, so no verdict on a file this diff did not touch depends on this diff. Result: ESLINT_EXIT=0, ERRORS=0 WARNINGS=0.
  • CI itself. Reported at draft-PR time by contract; the gate jobs on this PR had not converged when this body was written.

Draft on purpose: scripts/pm/** is governed, so this stays draft and merges by hand; review requests are the PM seat's step. skip-changeset is applied — the diff is one self-test case in a script under scripts/pm/** and publishes nothing from any package, which is the shape scripts/check-empty-changeset.mjs enumerates for the label (It releases nothing (.github/, .claude/, skills/, docs/, content/, examples/, tests-only, and the like) -> ... apply the 'skip-changeset' label). The label was written as a union with what the PR already carried and read back afterwards: size/s, skip-changeset, nothing stripped.

Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

🤖 Generated with Claude Code

https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1

…e self-test
One case walks the two roots the promise covers — the pm-dispatch skill tree
and this tool's own directory, both DERIVED (the directory of the mandate glob
naming the skill's main file; this module's own directory) rather than spelled,
so the population follows a rename and no fresh path literal joins this file's
watch-hint set. It counts occurrences of the tier constant's own VALUE, read
from the constant at run time — never a second literal, never a family or
prefix pattern — and requires exactly one, at the definition line, reporting
every offending file:line when it is not.
Until now that promise was held by a git grep someone remembers to run; the
drift it exists against was sixteen spellings across these same two roots.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026 — with Claude
…wed import
`relative` from `node:path` is bound to a fixture STRING inside `selfTest()`,
a few thousand lines above the new case, so calling it there is a TypeError at
run time rather than a name error at parse time — measured: the battery died
on the case's first statement with `relative is not a function`, after 1240
green cases. The repo-relative spelling now comes from ROOT, which every
sibling case in this battery already joins against, and the own-source read
goes through `import.meta.url` directly. A ROOT prefix that stops holding
leaves an empty root, which the walk reports instead of turning into a pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants

@os-litant@os-zhuang@claude