Repository navigation
Commit 4073030
fix(scripts): read the self-test handshake at two injection points, so a flag above the returned block is no longer scored HELD (#19026)
Fixes #18987
Clause-②: no
The early-return probe in `scripts/measure-self-test-floor.mjs` read ONE
injection point —
`return;` as the first statement of the dispatched self-test function —
and published a
three-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 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.
## The prerequisite: the instrument refused to run at all
`node scripts/measure-self-test-floor.mjs` exits 1 on `main` — census,
`--probe` and
`--probe --only` alike — because `classifyHandshake` reads three landed
spellings as `none`
while `LANDED_HANDSHAKE_NAMES` sees them, and the live completeness
check refuses rather than
printing 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:
| file | spelling |
|:--|:--|
| `scripts/check-workflow-step-name-quoting.mjs` | `const verdict =
selfTest();` then `if (verdict !== SELF_TEST_VERDICT)` |
| `scripts/pm/check-expected-skips.mjs` | `const r = selfTest();` then
`if (r.verdict !== SELF_TEST_VERDICT)` |
| `scripts/pm/check-prior-rulings.mjs` | same record shape |
**`LANDED_HANDSHAKE_NAMES` needed no addition** — its `*VERDICT`
comparison pattern already
matches 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.mjs`
binds `const selfTestCode = selfTest();`, refuses on its FLAG one line
later, and then compares
that binding against `EXIT_PREREQUISITE_NOT_MET` inside
`process.exit(cmp ? a : b)` to choose
which 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 a
fixture 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 own
`scripts/check-single-claim-paths.mjs`** (PR #18986 has not
landed, 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`:
```text
before (this branch's first commit, one injection point) -- exit 0
0 DEFEATED, 1 HELD, 0 ACCIDENT, 0 NOT MEASURED.
HELD scripts/check-single-claim-paths.mjs -- ✗ check-single-claim-paths self-test: selfTest() returned without reaching its verdict,
after (two injection points) -- exit 0
0 DEFEATED, 0 HELD, 1 HANDSHAKE-ABOVE-BLOCK, 0 ACCIDENT, 0 NOT MEASURED.
1 of the 1 probed return a block and were read at BOTH injection points;
HAB scripts/check-single-claim-paths.mjs -- point 1 refused (✗ check-single-claim-paths self-test: selfTest() returned wi)
point 2, inside the block it returns: exited 0 printing 0 byte(s)
```
**Positive control — PR #18986's head `03310bc914`**, the same file with
the handshake moved
inside 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):
```text
{ "verdict": "HELD", "points": 2,
"first": { "verdict": "HELD", "baselineExit": 0, "mutatedExit": 1, "mutatedBytes": 210 },
"second": { "verdict": "HELD", "baselineExit": 0, "mutatedExit": 1, "mutatedBytes": 210,
"mutatedHead": "✗ check-single-claim-paths self-test: selfTest() returned without reaching its verdict," } }
```
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 own
statement 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: 1`
and the reason it has
one, rather than an absent field.
Unadmitted, each with its reason in the docblock rather than for want of
a regex:
- an `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-test
still reaches its own verdict, which is HOLE 1, and the two holes are
never summed;
- a returned `.then(callback)` and an `async function` expression: 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 battery
floor — a `return;` immediately after the flag leaves both runs printing
the identical 18566
bytes 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
on `main`). Its controls
run inline on every invocation: **119 -> 154 control assertions**.
- Ablations, each from the committed state through
`scripts/ablation-replace.mjs`, each restored
with the blob hash equal to HEAD's and `git diff HEAD` empty:
- compose the second point's DEFEATED as HELD -> 2 controls red (`a
handshake set ABOVE the
block the self-test returns read HELD`);
- drop the anchor's LINE-START rule -> anchor control red (anchor moved
627 -> 442);
- drop the anchor's DEPTH rule -> anchor control red (anchor moved 627
-> 512);
- drop the `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/objectstack` at `e39670e33`:
26 families derived, all 26 run, **26 exit 0**; `--ran` reconciliation
`✓ 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, so
no narrowing to declare.
- No package is touched, so there is no build closure and no package
test to run.
## Changeset
`skip-changeset`. Nothing published moves: the only changed path is
`scripts/measure-self-test-floor.mjs`,
which lives in the private root package (`@objectstack/spec-monorepo`,
`private: true`), and no
published 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 control `defineStack` ->
395.
## The census document is deliberately untouched
`docs/audits/2026-09-self-test-shape-census.md` is the instrument's own
output and is NOT
regenerated here. Its population is 179 rows at `d30ccb9bd`; the census
is 223 rows today, and
its 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 `main` it exits 1 printing no census at all, and on this branch it
prints
`1 HANDSHAKE-ABOVE-BLOCK` with `point 2 ... exited 0 printing 0 byte(s)`
— the reading the
census'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.mjs` sets `selfTestReachedVerdict =
true` at its line 2849 and
then 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 handshake
reading "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 the
sentence its author wrote — after an early return the binding is
`undefined` and the property read
throws. 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 the
recogniser's docblock. Next toucher: whoever widens `classifyHandshake`
again.
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](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9bdc384 commit 4073030
1 file changed
Lines changed: 700 additions & 13 deletions
0 commit comments