Uh oh!
There was an error while loading. Please reload this page.
test(pm): pin the contract-review tier's one-value-site promise in the dispatch-gates self-test (#14616) - #14672
Merged
Conversation
…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
…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
This was referenced Sep 2, 2026
os-zhuang
approved these changes
Sep 2, 2026
os-zhuang
marked this pull request as ready for review
September 2, 2026 22:51
os-zhuang
enabled auto-merge
September 2, 2026 22:51
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#14616
One
check:pm-dispatch-gatesself-test case, in the tier region ofselfTest()inscripts/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.mjsis 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:
dirname()of the mandate glob that names the skill's main file, taken fromMANDATORY_TIER_GLOBS, a population this module already declares;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 onet()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 two guards named in the filing, both measured
1. Watch-hint extraction and the declared population. This module's own
extractWatchHintsrun over its own source, before and after the edit: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
maskSelfTestsblanks theselfTest()body before extraction, which is what makes any fixture path in that region inert in the first place. The module'sinherited-populationdeclaration is untouched and still true —declaredInheritedPopulation()over the edited source still answers.github/workflowsand 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:
2. The verbatim maintainer quotes stay out of reach. The rulings quoted in the skill's main file and in
references/dispatch-runbook.mdname 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
.mjsscript, 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: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 bygit statusand bygit 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 passat2aa8456cbefore the edit,1241 cases passafter — 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:
relativefromnode:pathis bound to a fixture STRING insideselfTest(), thousands of lines above the new case, so the shadowing is invisible at parse time andnode --checkpasses. The second commit takes the repo-relative spelling fromROOTinstead — the prefix every sibling case in this battery already joins against — and reads its own source throughimport.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:relativeandresolveare the only twonode:pathnames 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 HEADin that run; the tree was clean and no commit followed).pnpm check:pm-dispatch-gates✓ dispatch-gates self-test: 1241 cases pass.(legs 1 and 3)pnpm check:watch-hint-literal✓ 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-live✓ 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-guard✓ 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-guard✓ check:parse-guard: 198 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.node scripts/check-self-test-wired.mjs✓ 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.mjsOK: all 130 declared cross-package glob(s) (92 unique) are covered by core or crosspkg …node scripts/check-cross-package-test-inputs.mjsOK: 25 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.pnpm check:cross-package-test-inputsnode scripts/check-shard-attestation.mjs✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).pnpm check:agent-test-spelling✓ 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-floor✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct outside a comment …pnpm check:cli-command-ids✓ 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-targets✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 32 file(s) resolve against 79 workspace package(s) …node scripts/check-nul-bytes.mjscheck-nul-bytes: OK (scanned 7999 text file(s) … no raw ASCII control bytes).node scripts/check-test-completeness.mjscheck:nul-bytesis 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.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 HEADreturns exactly one path,scripts/pm/dispatch-gates.mjs; (2) the file count came from eslint's own--format jsonoutput,FILES_LINTED=1with 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 singleeslint.config.mjssays of itself that itnever 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.Draft on purpose:
scripts/pm/**is governed, so this stays draft and merges by hand; review requests are the PM seat's step.skip-changesetis applied — the diff is one self-test case in a script underscripts/pm/**and publishes nothing from any package, which is the shapescripts/check-empty-changeset.mjsenumerates 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