Skip to content

fix(scripts): check-release-section-coverage no longer renders a could-not-read as a floor-mismatch verdict - #14160

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-13954-cwd-false-verdict
Sep 1, 2026
Merged

fix(scripts): check-release-section-coverage no longer renders a could-not-read as a floor-mismatch verdict#14160
baozhoutao merged 1 commit into
mainfrom
claude/issue-13954-cwd-false-verdict

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #13954

scripts/check-release-section-coverage.mjs --self-test reads its sibling gate to pin the two
scope floors equal. The path was repo-root-relative and the read sat inside a swallowing
catch, so from any working directory but the repo root the read failed, siblingFloor stayed
null, and the case reported a content verdict about the other gate — in the exact prose a
real cross-gate floor drift produces. Exit 1 on a clean tree, at the same commit that exits 0
from the repo root.

Per the triage ruling on the card, both remedies land, and the diff is fenced to that one
file. The "do sibling gates share this shape" sweep is another card and is not folded in
here; check-release-page-status.mjs is untouched.

What changed

  1. The sibling is resolved from import.meta.url. SIBLING_GATE stays the repo-relative
    name every message prints (a reader is told where to look in the tree); a new
    SIBLING_GATE_PATH, resolved from this file, is what the read actually opens. An absolute
    path cannot depend on process.cwd(). import.meta.url was already load-bearing here (the
    entry guard at the bottom of the file), so this is the same seed used one step earlier.

  2. The swallowing catch is gone. readSiblingFloor() answers with either a floor or an
    unmet prerequisite — there is deliberately no third "no floor, carry on" value, because that
    value is precisely what used to reach the comparison and come back out of it as a verdict.
    Two branches refuse: an unreadable sibling, and a readable one that no longer declares a
    floor to inherit. Both exit 3 in the shared PREREQUISITE NOT MET frame
    (check-test-completeness.mjs / import-prerequisite.mjs — same number, same clauses),
    printed before the first case runs. One clause is this gate's own and is the point of the
    card: the refusal states in words that it is not the claim that the two gates floor at
    different majors.

Exit codes are now named constants (EXIT_OK / EXIT_FINDINGS / EXIT_PREREQUISITE_NOT_MET)
rather than spelled inline, so the self-test pins the value each path actually returns. Only the
prerequisite branch gets a code of its own — a broken instrument is still this gate's own exit
1, because that is a measurement that was attempted over the corpus and came back impossible.

Self-test: 41 to 50 cases

The new cases pin behaviour and names, not a count:

  • scope/read — the sibling path is absolute and equals resolve(REPO_ROOT, SIBLING_GATE).
  • scope/cwdthe defect, observed rather than argued: the floor reads the same after
    process.chdir() to an unrelated directory (cwd restored in a finally).
  • scope/refusal (x5) — a throwing read is an unmet prerequisite carrying the reason; a
    readable sibling with no floor declaration is the same class; the refusal exits 3, distinct
    from 1 and from 0; a floor that WAS read produces no refusal (a refusal that fires always is
    not a refusal); the text says nothing was measured, says it is not a finding, denies the
    floor-disagreement reading by name, and names both codes.
  • scope/verdict (x2) — two source-level pins with a shared negative control fixture: no
    case in the battery can render the old could-not-read placeholder into a verdict again, and
    the refusal interpolates its exit codes rather than spelling them. The control carries both
    forbidden shapes so each pin is proven able to fail.

Evidence — both directions, exit codes captured with no pipe in between

Before, at the merge base, on a clean tree:

cd .../objectstack-issue-13954 && node scripts/check-release-section-coverage.mjs --self-test
  OK  self-test: 41 cases pass ...                                       EXIT=0
cd .../          && node objectstack-issue-13954/scripts/...--self-test
  x self-test: scope — ... (sibling: [could-not-read], here: 16) ...
  check-release-section-coverage --self-test: 1 failure(s).              EXIT=1

After, same tree, three working directories:

repo root         EXIT=0     50 cases pass
its parent        EXIT=0     50 cases pass
/                 EXIT=0     50 cases pass

The loud prerequisite path, observed end to end in a scratch copy (the repo tree was never
mutated) — with a control that separates the fixture from the cause:

sibling ABSENT                       EXIT=3  PREREQUISITE NOT MET — ... could not be read.
sibling present (the real file)      EXIT=0  50 cases pass          <- control
sibling present, declaration renamed EXIT=3  PREREQUISITE NOT MET — ... declares no
                                             SCOPE_FLOOR_MAJOR to inherit.

The mutation was confirmed on disk before each reading (0 occurrences of the old declaration, 1
of the renamed one), not inferred from the editor's exit code.

Ablation of remedy 1, in a throwaway copy: undoing only the import.meta.url resolution
(back to the bare relative spelling) turns the copy from EXIT=0 to EXIT=1 with exactly the two
new cwd pins red — scope/read and scope/cwd. So those pins can fail, and they fail on the
regression they exist for.

Gates, all run at d6fdbb5e (the head this PR opens with)

Family derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (the script reads its own change set from the merge base), then run
with each exit code captured before any pipe:

0  node scripts/check-ci-filter-parity.mjs             0  pnpm check:cross-package-test-inputs
0  node scripts/check-cross-package-test-inputs.mjs    0  pnpm check:entry-guard
0  node scripts/check-release-section-coverage.mjs     0  pnpm check:parse-guard
0  node scripts/check-shard-attestation.mjs            0  pnpm check:pm-dispatch-gates
3  node scripts/check-test-completeness.mjs            0  pnpm check:pnpm-filter-targets
0  node scripts/pm/bare-root-worklist.mjs --self-test  0  pnpm check:watch-hint-literal
0  node scripts/render-release-coverage-anchor.mjs --self-test
0  pnpm check:agent-test-spelling   0  pnpm check:bash32-floor   0  pnpm check:cli-command-ids
0  node scripts/check-ratchet-remedy-authority.mjs     0  node scripts/check-nul-bytes.mjs
0  node scripts/check-release-section-coverage.mjs --self-test
0  pnpm lint   (repo-wide `eslint . --no-inline-config`, not narrowed)

check-test-completeness.mjs's 3 is NOT MEASURED, not a red: the derived family names it
with no argument and there is no local turbo run test log to grade. Its own header documents
that branch as the one the family hands out. Reading it as a finding is the very confusion this
PR is about, one gate over.

The two convention-triggered obligations for editing a gate script both pass:
bare-root-worklist.mjs --self-test and check:pm-dispatch-gates. The edited script's own test
suite is its --self-test (green, 50 cases); no *.test.ts in the tree names this script.

Is the fix CI-observable? No, and deliberately no rider

Every invocation of this gate runs from the repo root: lint.yml (Release section-coverage
guard), release-coverage-patrol.yml (Sweep the corpus) and release.yml (run_gate). None of
the three workflows carries a working-directory: key anywhere. So no CI job could have hit the
false verdict, and none exercises the new path either. Adding a non-root invocation to a
workflow to make it observable would be a rider on this card, so it is reported here instead.
--self-test is only ever invoked in lint.yml, where an exit 3 would fail the step — correct:
a pin that genuinely cannot be evaluated is not a pass.

The gate's non-self-test behaviour is unchanged: it does not read the sibling at all, and
exitCodeFor still answers only 0 or 1. The patrol reads that exit code as "did the instrument
read the corpus" and is unaffected.

Changeset

None, and skip-changeset is applied instead. The diff is one repo-internal gate script: the
root package is private, and every published package ships ["dist", "README.md", "CHANGELOG.md"] only, so nothing here reaches an npm consumer and there is no package a
changeset could honestly bump. Declared deviation: the dispatch brief's closed list for that
label names scripts/pm/** rather than scripts/*.mjs; this file is the same species
(repo-internal tooling that publishes nothing) and the substantive criterion holds, so the label
is applied and the deviation is stated here rather than made silently.

What was deliberately not touched

SPEC_CHANGELOG, RELEASES_DIR and INDEX_PATH remain cwd-relative. They belong to main(),
which operates on the repo tree and, from a non-root cwd, dies loudly on an unhandled ENOENT
rather than rendering a verdict. That is a different class from the one this card names, and
changing it would alter the gate's behaviour beyond the fence.

Generated by Claude Code


Generated by Claude Code

…ot-read as a floor verdict

The self-test reads the sibling gate to pin the two scope floors equal. The
path was repo-root-relative and the read sat inside a swallowing `catch`, so
from any cwd but the repo root the read failed, the floor stayed null, and the
case reported a content verdict about the OTHER gate — in the exact prose a
real cross-gate floor drift produces. Exit 1 on a clean tree, at a commit that
exits 0 from the repo root.

Both halves, because either alone leaves the defect reachable:

1. The sibling is resolved from `import.meta.url`, so the read no longer
   depends on where the process was started.
2. The `catch` no longer feeds null into the comparison. An unreadable sibling,
   or one that no longer declares a floor, REFUSES: exit 3 in the shared
   PREREQUISITE NOT MET frame (check-test-completeness.mjs /
   import-prerequisite.mjs), before the first case runs.

Self-test grows from 41 to 50 cases: the cwd-independence is observed from an
unrelated working directory rather than argued, both refusal branches and the
exit-code class are pinned, and two source-level pins with a shared negative
control forbid the placeholder shape and a hardcoded exit code from coming
back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actions github-actions Bot added the size/m label Sep 1, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 1, 2026 09:36
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit c69bd39 Sep 1, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13954-cwd-false-verdict branch September 1, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants