fix(scripts): read the self-test handshake at two injection points, so a flag above the returned block is no longer scored HELD - #19026
Conversation
`classifyHandshake` read only the DIRECT comparison of a self-test's return value (`selfTest() !== SELF_TEST_VERDICT`). Three landed files bind the value first and compare the binding, or one field of it, a statement later: const verdict = selfTest(); if (verdict !== SELF_TEST_VERDICT) -- check-workflow-step-name-quoting const r = selfTest(); if (r.verdict !== SELF_TEST_VERDICT) -- check-expected-skips, check-prior-rulings All three read `none`, `LANDED_HANDSHAKE_NAMES` saw them, and the live completeness check therefore refused the WHOLE census on every invocation -- the census, `--probe` and `--probe --only` alike, exit 1, nothing printed. Measured delta over the 223-row census: exactly those three rows move `none -> sentinel`; 103 flag, 1 helper, 104 sentinel, 15 none after. `LANDED_HANDSHAKE_NAMES` needs no addition -- its `*VERDICT` comparison pattern already sees all three, which is why the disagreement was one-sided. The GUARD half is a boundary the measurement forced: admitted without it, a fourth row moved -- `check-plugin-teardown-shape.mjs`, which binds the return value, refuses on its FLAG, and then compares the binding against a named constant inside `process.exit(<cmp> ? a : b)` to choose an exit code. Choosing a value is not asking whether the self-test finished, so the bound spelling reads a comparison that HEADS an `if (...)` and nothing else. Controls, both directions on each: both carrier shapes read `sentinel`; neither fixture matches the direct pattern; a literal operand reads `none` in both shapes (the ACCIDENT boundary, one property deeper); the exit-code selection fixture reads `flag` while still carrying the binding; bound and compared INSIDE the self-test reads `none`; the spelling written only in a comment reads `none` and reads `sentinel` un-hidden; and both new fixtures survive the name-independence rename. Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF Co-authored-by: Claude <noreply@anthropic.com>
…o a flag above the returned block is no longer a HELD
The probe injected `return;` as the FIRST statement of the dispatched function
and read one answer from it. A handshake set ABOVE the block that function
RETURNS stays unset under that injection, the dispatch refuses, and the row
read HELD -- while the same `return;` one statement lower, inside the block,
leaves the handshake SET and the run silent at exit 0. The 2026-09 census
cleared `scripts/check-single-claim-paths.mjs` HELD exactly that way, on the
revision that carried that ordering defect.
A file whose dispatched function returns a block is now read at both points and
HELD requires both. `HANDSHAKE-ABOVE-BLOCK` is the new verdict for "point 1
refused, point 2 exited 0", counted apart from HELD as ACCIDENT already is.
Measured on this base (223 rows, 217 with a resolvable entry): ONE row carries a
second point -- `check-single-claim-paths.mjs` -- so no other row's reading
moves, and every other row publishes `points: 1` with the reason it has one.
before --probe --only scripts/check-single-claim-paths.mjs -> 0 DEFEATED, 1 HELD, exit 0
after same command -> 1 HANDSHAKE-ABOVE-BLOCK, exit 0; point 1 refused,
point 2 exited 0 printing 0 bytes
control PR #18986's head (03310bc, the same file with the handshake moved
inside the block) -> HELD at BOTH points, both baselines exit 0
The anchor is the returned `async () => {` IIFE at the dispatched function's own
statement level: masked, line-anchored and at depth 0. Deliberately unadmitted,
each with its reason in the docblock: an `await (async () => {` in the middle of
the body (an early return there skips a battery while the self-test still
reaches its verdict -- hole 1, never this one), a returned `.then()` callback
and an `async function` expression (no carrier in this tree). The header also
records what the point CANNOT read: work whose failure is silent, measured on
`check-system-context-census.mjs`, whose handshake sits 48 lines above its own
battery floor and where both runs print the same bytes.
Costs: the second pair of spawns runs only for a row that HELD at the first
point AND has a second point, so a red-baseline checkout costs what it did.
Controls, both directions: the pair of fixtures differing only in WHERE the
handshake line sits (above -> HANDSHAKE-ABOVE-BLOCK, inside -> HELD at both);
five anchor decoys ahead of the real block, one per rule; an awaited mid-body
block and a block-free self-test both yielding no second point; a one-point row
reading identically through the composer and through the probe; and a row
DEFEATED at the first point probed exactly once.
Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
…with a decoy each
An ablation of the LINE-START rule came back GREEN: the only mid-line decoy in
the fixture was also nested, so the DEPTH rule covered for it and deleting a
real rule moved no verdict -- the shape of false green this instrument exists
to refuse, made by its own controls.
Two decoys now isolate the rules, each a branch a passing run never enters:
a one-line `if (...) return (async () => {` at the body's own depth (only
LINE-START excludes it) and a line-anchored return one level deep inside an
`if` block (only DEPTH does). Their validity is asserted the way the anchor
reads them -- masked text, depth counted from the definition's own brace --
and an unanchored, depth-blind reading is pinned to land on a decoy.
Re-measured after the change: ablating either rule reds the anchor control.
Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
…lf-test-floor-second-injection
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Landing record — every pre-check met, blocked at the channel (skills seat, Every check on Generated by Claude Code |
|
Landing record — landed on the seat's record through the CCR route (skills seat, Channel: the two allow rules of PR #19047 ( Acts: Generated by Claude Code |
Fixes #18987
Clause-②: no
The early-return probe in
scripts/measure-self-test-floor.mjsread ONE injection point —return;as the first statement of the dispatched self-test function — and published athree-valued verdict from it. A handshake set ABOVE the block that function RETURNS stays
unset under that injection, the dispatch refuses, and the row reads HELD, while the same
return;one statement lower, inside the block, leaves the handshake SET and the run silentat exit 0. The 2026-09 census cleared
scripts/check-single-claim-paths.mjsHELD exactly thatway, on the revision that carried that ordering defect.
A file whose dispatched function returns a block is now read at BOTH points, and HELD requires
both.
HANDSHAKE-ABOVE-BLOCKis the new verdict for "point 1 refused, point 2 exited 0",counted apart from HELD as ACCIDENT already is.
The prerequisite: the instrument refused to run at all
node scripts/measure-self-test-floor.mjsexits 1 onmain— census,--probeand--probe --onlyalike — becauseclassifyHandshakereads three landed spellings asnonewhile
LANDED_HANDSHAKE_NAMESsees them, and the live completeness check refuses rather thanprinting the smaller of two disagreeing populations. That is the defect #18329 records, and
nothing about this card could be measured without it, so the first commit widens the
recogniser exactly as far as running the probe needs. #18329 remains open and this PR does not
address it beyond that prerequisite; the seat decides what is left of it.
The three spellings, all of them the return value bound before it is compared:
scripts/check-workflow-step-name-quoting.mjsconst verdict = selfTest();thenif (verdict !== SELF_TEST_VERDICT)scripts/pm/check-expected-skips.mjsconst r = selfTest();thenif (r.verdict !== SELF_TEST_VERDICT)scripts/pm/check-prior-rulings.mjsLANDED_HANDSHAKE_NAMESneeded no addition — its*VERDICTcomparison pattern alreadymatches all three, which is why the disagreement was one-sided. The refusal text prescribes
widening both "with a control on each"; the measurement says only one of the two was short,
and adding a spelling to the second opinion that it already reads would weaken it, not widen
it.
The GUARD half is a boundary the measurement forced. Admitted as "the binding compared
against a named operand anywhere", a FOURTH row moved:
scripts/check-plugin-teardown-shape.mjsbinds
const selfTestCode = selfTest();, refuses on its FLAG one line later, and then comparesthat binding against
EXIT_PREREQUISITE_NOT_METinsideprocess.exit(cmp ? a : b)to choosewhich code to exit with. Choosing a value is not asking whether the self-test finished, so the
bound spelling reads a comparison that heads an
if (...)and nothing else. Its control is afixture reduced from that file.
Delta over the 223-row census: exactly the three rows move
none -> sentinel. Before:101 sentinel, 103 flag, 1 helper, 18 none. After: 104 sentinel, 103 flag, 1 helper, 15 none.
The two controls, with exit codes
Negative control —
main's ownscripts/check-single-claim-paths.mjs(PR #18986 has notlanded, so the flag still sits above the returned block). Through the shipped path,
node scripts/measure-self-test-floor.mjs --probe --only scripts/check-single-claim-paths.mjs:Positive control — PR #18986's head
03310bc914, the same file with the handshake movedinside the block as its last statement, probed in a second worktree at that revision (fetched
into a ref of this session's own, never merged into this branch):
Both legs' baselines exit 0, so both readings are available rather than inherited from a red
tree.
The anchor, and what it deliberately does not read
The second point is the returned
async () => {IIFE at the dispatched function's ownstatement level: read over the comment-and-literal mask, line-anchored, and at brace depth 0.
Measured over the census: 217 rows resolve an entry and exactly ONE carries a second point, so
no other row's reading moves; every one-point row publishes
points: 1and the reason it hasone, rather than an absent field.
Unadmitted, each with its reason in the docblock rather than for want of a regex:
await (async () => {in the MIDDLE of the body (check-half-states.mjs,check-governed-queue-guard.mjs): an early return there skips a battery while the self-teststill reaches its own verdict, which is HOLE 1, and the two holes are never summed;
.then(callback)and anasync functionexpression: no carrier in this tree.The header also records what this point CANNOT read, because a reader will otherwise take a
HELD for more than it is: work after the handshake whose failure is SILENT. Measured on
scripts/check-system-context-census.mjs, whose handshake sits 48 lines above its own batteryfloor — a
return;immediately after the flag leaves both runs printing the identical 18566bytes at exit 0, so a mutation reports "no observable effect" and no probe can stand in for
reading the ordering. That file is in the acceptance notes below.
Tests
node scripts/measure-self-test-floor.mjs— exit 0, 223 rows (exit 1 onmain). Its controlsrun inline on every invocation: 119 -> 154 control assertions.
scripts/ablation-replace.mjs, each restoredwith the blob hash equal to HEAD's and
git diff HEADempty:a handshake set ABOVE the block the self-test returns read HELD);if (...)guard from the bound sentinel -> boundary control red(
read sentinel, not flag).The first ablation of LINE-START came back GREEN and that is in the history: the only mid-line
decoy was also nested, so DEPTH covered for it. Two decoys now isolate the rules, one each, and
the re-run reds.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackate39670e33:26 families derived, all 26 run, 26 exit 0;
--ranreconciliation✓ 26 derived famil(ies) accounted for — 26 run, 0 NOT-MEASURED.pnpm lint(the whole repo,eslint . --no-inline-config): exit 0 in 1m46s — the full run, sono narrowing to declare.
Changeset
skip-changeset. Nothing published moves: the only changed path isscripts/measure-self-test-floor.mjs,which lives in the private root package (
@objectstack/spec-monorepo,private: true), and nopublished package's
files[]carries an entry escaping its own directory (measured: 0 of them).Symbol grep over
packages/:returnedBlockAnchor,probeVerdictPoints,measure-self-test-floor-> 0 files; positive controldefineStack-> 395.The census document is deliberately untouched
docs/audits/2026-09-self-test-shape-census.mdis the instrument's own output and is NOTregenerated here. Its population is 179 rows at
d30ccb9bd; the census is 223 rows today, andits probe column is a full sweep this container cannot run in one foreground turn. Regenerating
it would move rows for drift and for the recogniser widening as well as for this point, which
makes "which rows moved" unreadable — the one thing a regeneration owes. A regeneration is its
own card, and this PR's readings above name the row that changes:
check-single-claim-paths.mjs,HELD -> HANDSHAKE-ABOVE-BLOCK.
Reader test
Run
node scripts/measure-self-test-floor.mjs --probe --only scripts/check-single-claim-paths.mjs:on
mainit exits 1 printing no census at all, and on this branch it prints1 HANDSHAKE-ABOVE-BLOCKwithpoint 2 ... exited 0 printing 0 byte(s)— the reading thecensus's HELD did not take.
Acceptance notes
To file (class b, a declared-contract violation; dedupe words:
check-system-context-census handshake above floor·selfTestReachedVerdict before battery floor·success line printed before the floor evaluates·self-test verdict ordering static·handshake not the last statement):scripts/check-system-context-census.mjssetsselfTestReachedVerdict = trueat its line 2849 andthen runs 48 lines of battery floor, with its success line printed ABOVE the flag. AGENTS.md states
「Set the flag as the self-test's last statement, after its success line prints」, and the file's own
comment claims the floor is 「Evaluated after every battery has had its chance and BEFORE the
verdict」, which its own text contradicts. An early exit anywhere in that floor region leaves a run
that printed
all cases passed, exited 0, and never evaluated its floor — with the handshakereading "reached". Measured here: the mutation is invisible to this probe (identical 18566 bytes,
both exit 0), so this is a static ordering finding and the instrument correctly reports that it
observed nothing.
noted, not filed: the record spelling of the bound sentinel
(
const r = selfTest(); if (r.verdict !== ...)) refuses with a TypeError rather than with thesentence its author wrote — after an early return the binding is
undefinedand the property readthrows. It is a real, loud non-zero exit and the probe publishes whatever the run said, so the
column's answer (
sentinel) is correct; the wording is a property of the shape, recorded in therecogniser's docblock. Next toucher: whoever widens
classifyHandshakeagain.noted, not filed: this instrument's docblock still quotes "181 rows" and "158 differently shaped
self-tests" from an older base against today's 223. Prose drift in a file this PR already touches;
correcting it here would bury the diff that matters. Next toucher: whoever regenerates the census
document.
Generated by Claude Code