Repository navigation
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 |
…s verdict, and set the handshake last (objectstack-ai#19053) Fixes objectstack-ai#19029 Clause-②: no One file: `scripts/check-system-context-census.mjs`. A root `scripts/` gate, nothing published. ## Premise, re-read on the source first The card was filed bare and triage (comment 5730522407) asked the taker to read the order itself and refute the card if it was in fact compliant. Read on `origin/main` `b22db516d`: `selfTest()` printed its verdict line, set `selfTestReachedVerdict = true`, and only THEN evaluated its 46-line battery floor, whose own comment claimed it was "Evaluated ... BEFORE the verdict". Premise **confirmed**, and `AGENTS.md`'s `Writing a --self-test` band is explicit: "Set the flag as the self-test's last statement, after its success line prints". ## What moved - The floor block moves **above** the verdict write. Its 40 lines of code are **byte-identical** after the move (sha256 of the block `f6312a3aa320cbf0` before and after); the roster, the floor's rules and `SELF_TEST_BATTERY_FLOOR` are untouched, and so is `run()` and the entry block. - `selfTestReachedVerdict = true` becomes the **last statement** before the return, after the verdict line prints — the ordering `scripts/check-agent-model-declared.mjs` already carries (copied, ⛔ not imported). - The floor's comment now describes the order it relies on, and names why the order is load-bearing. - One new self-test case, `⭐ VERDICT ORDER`, filed under the battery already open — the roster is **not** widened. ## Why the new case is a STATIC read, and what it pins The floor prints only when it FAILS, so a passing run's bytes are identical whichever side of the verdict it sits on: no mutation of a green tree exposes the order, which is why the fleet probe on PR objectstack-ai#19026's branch correctly reports that it observed nothing. So the case reads the order off this file's own text (the idiom the neighbouring `POPULATION DECLARATION` pins already use) and holds five facts, each anchor required to resolve **exactly once**: ``` {"floorBeforeVerdict":true,"floorAddsToTheCounterTheVerdictReads":true,"flagAfterVerdictLine":true, "nothingExitsBetweenVerdictAndFlag":true,"flagIsTheLastStatement":true, "anchorsResolvedExactlyOnce":{"floor":true,"verdict":true,"flag":true,"returnStatement":true}} ``` Chained, those are the behavioural claim: the floor adds to the same `failures` the verdict reads, the verdict prints after it, and the flag is the last statement with no `return` or `process.exit(` between — so a breached floor can only print the FAILED line and can only return non-zero. Comments are stripped before the two adjacency reads, in both directions: prose between the two statements must not red the case, and prose must not be able to hide a statement from it either (a line-anchored `^return` misses `if (x) return 0;` — measured, see leg 2). ## Measurements All four ablation legs ran through `scripts/ablation-replace.mjs` on the **committed** file: anchor hit count declared, the write verified against the disk and the blob hash, and the restore proven by `blob == HEAD` plus an empty `git diff HEAD`. Every leg printed `ok restored: blob == HEAD (dcaf90e) and git diff HEAD is empty`. **1. A passing run is unchanged except for the new case** (`--self-test` before vs after, full transcripts diffed): ``` 93a94 > ok ⭐ VERDICT ORDER: the battery floor is evaluated ABOVE the verdict line and ... ``` That is the whole diff: 95 lines / 18,636 bytes before, 96 lines / 19,201 bytes after, 80 cases to 81, success line still last, exit 0 both. Nothing else moved, because the floor prints nothing on a passing run. **2. A breached floor prints the FAILED line and no success line.** Mutation: one pin raised above its count (`'absence is loud': 1,` to `: 99,`). ``` ablation-replace: ok mutation landed: anchor 1 -> 0, blob dcaf90e -> 85fc130dabf9 FAIL self-test battery "absence is loud" registered 1 case(s), below its pinned floor of 99 — cases that used to run no longer do. FAIL A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the number. ... check-system-context-census --self-test: 2 case(s) FAILED ablation-replace: command exited 1 ``` No `all cases passed` anywhere in that run (grep count 0). Before this change the same breach printed `all cases passed` and then those FAIL lines. **3. An early exit between the verdict and the flag is caught twice.** Mutation: `return 0;` injected after the verdict write, before the flag. ``` FAIL ⭐ VERDICT ORDER: ... -- {"floorBeforeVerdict":true,...,"nothingExitsBetweenVerdictAndFlag":false,...} check-system-context-census --self-test: 1 case(s) FAILED ✗ check-system-context-census self-test: selfTest() returned without reaching its verdict, so no success line was printed. Exiting 0 here would report a self-test that never finished as a self-test that passed. ablation-replace: command exited 1 ``` The static case names the ordering; the handshake refuses the 0. Exit 1. **4. An early exit inside the floor region, new order.** Mutation: `return 0;` injected at the top of the floor region (before `const declaredBatteries`). ``` ok ⭐ VERDICT ORDER: ... (no verdict line at all: grep "all cases passed" = 0, grep "case(s) FAILED" = 0) ✗ check-system-context-census self-test: selfTest() returned without reaching its verdict, so no success line was printed. ... ablation-replace: command exited 1 ``` **5. ⭐ The identical injection on `origin/main`'s shape — the hole, empirically.** Same anchor, same replacement, run against `HEAD~1`'s bytes checked out on disk (blob `2b21f55e7045`, verified before mutating; restored to HEAD's `dcaf90ed4d1a` after, `git diff HEAD` empty): ``` ok: the origin/main shape is on disk (floor BELOW the verdict and BELOW the handshake) ablation-replace: ok mutation landed: anchor 1 -> 0, blob 2b21f55 -> ac63c9bade5a check-system-context-census --self-test: all cases passed ablation-replace: command exited 0 ``` A run that printed the success line, exited 0, and never evaluated its floor.⚠️ By the escalation rule the triage seat wrote into comment 5730522407 ("注入一次真实早退后该运行仍绿 ⇒ 升 p1"), that reading is the named trigger — it is a measurement, not a re-grade: the priority label stays triage's to move, and this PR touches no label but `skip-changeset`. ### Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **30** commands from this tree (change set: 1 path, merge base `b22db516d`). All 30 ran, exit codes captured redirect-then-`$?`, and the record reconciles with `--ran`. Both halves of the subject gate are in that list and green: `check-system-context-census: OK — 110 elevation read sites in 20 packages across 45 files ...`. Named NOT MEASURED by the derivation itself, ⛔ not by me: 2 families taking a value from the workflow, 1 path-scheduled CI job's 5 steps, 11 wide-population families, 14 pending-changeset families and the 53 artifact-roster families — CI's. ### Lint, narrowed with its three readings `pnpm lint` is `eslint . --no-inline-config`, a whole-repo scan CI owns. The narrowing: (1) the population is eslint's own — `eslint.config.mjs` lints `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` and this file is in it (it produced a result object, not an ignore skip); (2) `--format json` reports **1** file linted, 0 errors, 0 warnings, 0 suppressed, exit 0; (3) the config **never enables type-aware linting for ANY file** (`eslint.config.mjs`: "no `parserOptions.project`, no typed `@typescript-eslint` rules", measured there with a positive control), so a one-file diff cannot move an untouched file's verdict. All readings taken at `1f684af84`, the final commit. ## Reader test Run `node scripts/ablation-replace.mjs --file scripts/check-system-context-census.mjs --anchor " const declaredBatteries = Object.keys(SELF_TEST_BATTERIES);" --replacement " return 0; const declaredBatteries = Object.keys(SELF_TEST_BATTERIES);" -- node scripts/check-system-context-census.mjs --self-test`: it must print **no** verdict line and exit 1 (the dispatch says the self-test never reached its verdict). On `origin/main` that same command prints `all cases passed` and exits 0. ## Acceptance notes - Out of scope, noted and ⛔ not filed: `floorFailure`'s body carries a 6-space indent on its `process.stdout.write` where 4 is the file's shape. It moved byte-identically with the block rather than being tidied, so the diff stays reviewable as a move; a style nit, and the successor is whoever next edits this floor. - The self-test case is filed under the battery already open rather than a new roster entry, because the dispatch froze the roster. The cost is one case of slack in that battery's floor; the case sits after every one of that battery's own cases, so an early exit inside that block stops the new case too. - `docs/audits/2026-09-self-test-shape-census.md`'s row for this file (ROSTER / HELD) stays true: the handshake still holds, and legs 3 and 4 above are its live readings. --- _Generated by [Claude Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_ Co-authored-by: Claude <noreply@anthropic.com>
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