Skip to content

fix(scripts): give 24 more self-tests a verdict handshake at their dispatch (#13798 batch 2) - #14853

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-13798-self-test-handshake-b2
Sep 3, 2026
Merged

fix(scripts): give 24 more self-tests a verdict handshake at their dispatch (#13798 batch 2)#14853
baozhoutao merged 4 commits into
mainfrom
claude/issue-13798-self-test-handshake-b2

Conversation

@claude

@claude claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Part of #13798 (batch 2). Batch 1 is PR #14479, merged.

node scripts/measure-self-test-floor.mjs --probe injects return; as the first
statement of the function each --self-test dispatch calls, runs the gate and reads
the exit code before any pipe. Exit 0 is the defect: a self-test that never finished,
reported as one that passed.

What this batch does

The same boundary-only handshake batch 1 landed, transplanted to 24 more files. Two
shapes, one per dispatch spelling, both taken verbatim from the PR #13797 precedent:

  • sentinel (13 files) — selfTest() returns SELF_TEST_VERDICT only after its
    verdict line, and the dispatch refuses anything else. Used where the dispatch discards
    the return value. 10 files take it for the first time; the other 3 are the
    production-path additions below, on files that already carry the sentinel from batch 1.
  • module-level selfTestReachedVerdict flag (11 files) — used where the
    self-test's own exit code is load-bearing (process.exit(selfTest()),
    return selfTest(), the ternary form) or where the self-test already ends in
    process.exit and an appended sentinel would be dead code.

Nothing inside a self-test body changes beyond the verdict marker on the line after
the success line. The census's hole-1 floor classification is identical for all 165
files
before and after — the mechanical proof the remedy is boundary-only.

The probe, before and after

Measured on this branch's own merge base, not re-derived by hand.

population DEFEATED HELD NOT MEASURED
before (2d40f9146) 165 16 143 6
after 165 2 157 6

The 2 that remain are asserted equal as a set to the two files held by an open PR
and deliberately excluded here: scripts/check-type-check-coverage.mjs (PR #14805)
and scripts/pm/bare-root-worklist.mjs (PR #14800). Nothing else is left defeated.

14 of the 24 flipped DEFEATED -> HELD in the census. The other 10 are shapes the
census reads HELD or NOT MEASURED for reasons that are not a handshake, and each
carries its own evidence below.

The six that held only by accident

The card's own note: "5 gates hold today only by luck — a downstream TypeError or a
usage error, not a handshake."
The ACCEPT ruled them into this batch (Q1 → A). The
census verdict does not move for these (HELD before, HELD after), so the evidence is
the first line the mutated run prints — the accident replaced by the named refusal:

file mutated head, before mutated head, after
check-osv-exemptions.mjs file:///… (TypeError stack) the named refusal
typecheck-configs.mjs file:///… (TypeError stack) the named refusal
check-exported-any-returns.mts /home/… (TypeError stack) the named refusal
checklist-select.mjs usage: node scripts/checklist-select.mjs … (exit 2) the named refusal
run-with-stall-guard.mjs run-with-stall-guard: unknown option --self-test the named refusal
check-page-declaration-shape.mjs (printed nothing, exit 1) the named refusal

check-osv-exemptions.mjs needed one extra line beyond the transplant, in its own
commit: its dispatch destructures the result (const { passed, lines } = selfTest()),
so on an early return the destructuring threw before the handshake could run — the
accident survived the first attempt, and the census said so. The result is now read
into a local first and destructured after the handshake.

Three traps this surface carries

  1. Unbraced branch bodies. } else if (argv.includes('--self-test')) selfTest();
    followed by else run(); needs braces, or the trailing else re-binds.
  2. The temporal dead zone. scripts/run-with-stall-guard.mjs dispatches at the top
    of the file, above the self-test definition; the flag is declared above the
    dispatch, not next to the function.
  3. A never return type. check-exported-any-returns.mts declares
    function selfTest(): never. It takes the flag rather than the sentinel precisely so
    the annotation does not have to move — check-test-typecheck.mts in batch 1 is the
    precedent for how a sentinel return drifts a shrink-only DEBT ledger, and raising a
    ledger is maintainer-only.

The production-path half (ACCEPT Q2 → A)

check-filter-alias-parity.mjs, check-meta-type-normalized.mjs and
check-test-completeness.mjs also run their self-test on the production path and
discarded the result there — the same silence one seam over, and one the census never
drives because it only runs --self-test. Each now compares the sentinel on that path
too. Ablated by hand with the census's own injector, run with no --self-test on
the command line: baseline exit 0 / 0 / 3, mutated exit 1 / 1 / 1, each printing the
named refusal.

check-workspace-manifest-cycles.mjs is fixed but still NOT MEASURED

Its dispatch calls runSelfTest(), which wraps a selfTest() that returns a failure
list. The census picks an entry only when a file defines exactly one self-test-shaped
function or has an ENTRY_BY_HAND row; this file has two and no row, so it reads
NOT MEASURED (ambiguous entry) both before and after — the handshake is real, the
instrument just cannot see it.

Ablated by hand instead, with the census's own probeEarlyReturn:

  • entry runSelfTest (what the dispatch actually calls): baseline exit 0, mutated
    exit 1, head ✗ check-workspace-manifest-cycles self-test: runSelfTest() returned without reaching its verdict,HELD.
  • entry selfTest (what the census would pick if the ambiguity were resolved the
    wrong way): mutated exit 1 with a TypeError stack — an accident, not a handshake.

⚠️ The one-line 'scripts/check-workspace-manifest-cycles.mjs': 'runSelfTest' row that
would make this measurable lives in scripts/measure-self-test-floor.mjs, which is
outside this batch's declared file surface. It is not edited here; it is filed as
#14842 instead, with the two readings that say which entry the row must name.

Local verification

origin/main merged once more (a real merge, no rebase) before opening this PR. The gate
union was re-derived AFTER the final commit and run on that head, 61850abbf:

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands,
    DG_EXIT 0, 24 paths vs merge base f3ae441fa, 47 families, every one run from
    --commands. Plus the dispatch-named check:declared-population-live, the
    any-edit check:nul-bytes, and scripts/pm/check-governed-queue-guard.mjs --self-test
    (that file is edited here and governed-surface-guard.yml runs it, but the derivation
    did not list it).
  • 51 RAN-PASS, 1 NOT MEASURED. The one is
    node scripts/check-test-completeness.mjs — exit 3, PREREQUISITE NOT MET (it grades a
    saved turbo run test log and none exists locally). Never read as a pass.
    node scripts/check-engine-split-ratio.mjs first came back exit 2 on the shallow
    clone; after git fetch --shallow-since=2026-05-29 origin it is exit 0 with a real
    ratio, so it counts as RAN-PASS.
  • pnpm check:pm-dispatch-gates ran to completion: dispatch-gates self-test: 1241 cases pass.
  • pnpm build exit 0. pnpm lint over the WHOLE repo, not narrowed: exit 0, 5779 files,
    0 errors, 0 warnings (counts read from --format json).
  • Every one of the 24 files runs its own --self-test to the SAME exit code and
    byte-identical output as before the change (captured before the codemod, again after,
    then diffed). One file differs on one nondeterministic field:
    release-rehearsal-clone.mjs prints an elapsed-milliseconds figure (115 ms vs
    102 ms).
  • pnpm check:nul-bytes exit 0, plus a direct control-byte sweep of the touched files:
    no match.

Every exit code above was captured by redirecting first and reading $? after — never
through a pipe.

skip-changeset: scripts/** only, nothing published from any package.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LAwHpn4uVuf4N1geBcD5i3


Generated by Claude Code

…spatch

Batch 2 of the #13798 remedy: the boundary-only PR #13797 shape, transplanted
to the 14 files the probe still reads DEFEATED and released by their holding
PRs, the six that hold today only by accident (a downstream TypeError or a
usage error, not a handshake), the three that also call their self-test on the
production path and discard the result, and check-workspace-manifest-cycles.mjs
(runSelfTest() shape, NOT MEASURED by the census for want of an ENTRY_BY_HAND
row, defeated in fact).

Two shapes, one per dispatch spelling, both from batch 1 verbatim:
  - sentinel: the self-test returns SELF_TEST_VERDICT only after its verdict
    line, and the dispatch refuses anything else (where the return value is
    discarded);
  - module-level `selfTestReachedVerdict` flag (where the self-test's own exit
    code is load-bearing, or where it ends in process.exit and an appended
    sentinel would be dead code).

Nothing inside any self-test body changes beyond the verdict marker on the line
after the success line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAwHpn4uVuf4N1geBcD5i3
…ng it

The handshake was placed after `const { passed, lines } = selfTest()`, so an
early return made the destructuring throw a TypeError before the check could
run — the very accident that made this gate read HELD without a handshake. The
census now reports the named refusal instead of a stack trace.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAwHpn4uVuf4N1geBcD5i3
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 3, 2026
@github-actions github-actions Bot added the size/m label Sep 3, 2026
@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

PM status (domain:devx execution seat, session session_01LAwHpn4uVuf4N1geBcD5i3): reviewed and ACCEPTED on #13798 (batch-2 checklist there). Held as draft on one red check that is not this PR's: Validate Package Dependencies on 61850abbf — run 33719242879, job 100534794466 — fails only at step 12 "Audit dependencies for known vulnerabilities (OSV-Scanner)"; step 11 (this PR's own check-osv-exemptions.mjs --self-test + gate) passed. The workflow ran here because the PR edits scripts/check-osv-exemptions.mjs (path filter), and the scanner reads the base's lockfile, which still carries the four fast-uri advisories (#14732). The fix is PR #14783 (fast-uri 3.1.5 → 3.1.7), already in the merge queue. Plan: once #14783 lands, merge origin/main into this branch via update-branch (real merge commit), let the check re-run, then flip ready + arm. No re-run is requested for the red itself — it is deterministic on the current base.


Generated by Claude Code

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

PM action (domain:devx seat, session session_01LAwHpn4uVuf4N1geBcD5i3, closing shift): update-branch issued 10:3xZ — PR #14783 (the fast-uri lift that clears Validate Package Dependencies on the base) MERGED 08:38Z as b69da43d5. Next sitter: on GREEN flip ready + arm; any red on a shard this diff does not touch ⇒ read the anchor #14822 first.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 3, 2026 11:01
@baozhoutao
baozhoutao requested a review from hotlong as a code owner September 3, 2026 11:01
@baozhoutao
baozhoutao enabled auto-merge September 3, 2026 11:01
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit c47a5ac Sep 3, 2026
34 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13798-self-test-handshake-b2 branch September 3, 2026 12:00
os-litant pushed a commit that referenced this pull request Sep 3, 2026
Two conflicts, both against main commits that landed after this branch's
merge base 5ff5f95:

- .claude/skills/pm-dispatch/references/lanes/services.md: main's
  365141f (#14856) edited the very bullet this branch deletes (the
  bullet moved to references/platform-readings.md). Settled to OUR side:
  the bullet stays deleted and the move stands. The FACT main corrected in
  that bullet (the i18n gates answer PREREQUISITE NOT MET with exit 3, not
  1) is carried into the moved text by the follow-up commit, so this merge
  commit's non-conflict content stays a pure merge of the two parents.

- scripts/check-ratchet-remedy-authority.mjs: main's a610775 (#14896)
  added the self-test battery roster and floor at the spot where this
  branch's section (21) sits. Settled by keeping BOTH: section (21) opens
  with battery('(21) ...'), is declared in SELF_TEST_BATTERIES at its case
  count (2), and sits before the floor-evaluation block; the roster's own
  size pin SELF_TEST_BATTERY_FLOOR moves 20 -> 21 by the header's own
  convention (the roster size is pinned at the declared count). The
  exception phrase stays assembled, never spelled, in author-facing text,
  and the gate's run line is byte-identical to main's.

scripts/pm/check-skill-line-ratchet.mjs auto-merged with main's c47a5ac
(#14853) verdict handshake; the two run-path verdict lines this branch adds
are outside that handshake, which covers selfTest()'s sentinel return only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
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

Development

Successfully merging this pull request may close these issues.

2 participants