Skip to content

fix(scripts): check:single-claim-paths keeps its own two contracts — a failed board read exits PREREQUISITE NOT MET, and the self-test handshake is set by the verdict it certifies - #18986

Merged
os-elon-musk merged 2 commits into
mainfrom
claude/issue-18940-single-claim-paths-contracts
Sep 18, 2026
Merged

os-elon-musk merged 2 commits into
mainfrom
claude/issue-18940-single-claim-paths-contracts

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #18940

Clause-②: no

Two defects in one file, both of the same class — scripts/check-single-claim-paths.mjs
against the contracts it declares about itself. One commit each, so each is readable alone.
No other file is edited.

A. A transport failure exited 1, and this file defines 1 as the accusation

The live path read a PR's file list with nothing catching a throw. githubApi throws on any
non-ok response and fetch throws on a dead socket, so a 404, a dead credential or a dropped
connection escaped as an unhandled rejection and node exited 1 — which this file's own exit
register defines as "judged, an earlier open PR already claims a listed path". An accusation,
about a board the run never read, against an author who did nothing.

The file already forbade exactly that, in its own header: "a mis-wired gate must not read as an
accusation, because it would be red on every PR at once for something no author did." So this is
the file keeping its own rule, not a new policy.

The probe, run in an agent container. The card carried it unrun; this is the first run of it.

Before, at the base commit 26c73fb4e:

$ PR_NUMBER=99999999 GITHUB_REPOSITORY=objectstack-ai/objectstack node scripts/check-single-claim-paths.mjs
info  re-exec with --use-env-proxy: HTTPS_PROXY is set and node's fetch does not read it.
file:///home/user/objectstack-issue-18940/scripts/check-single-claim-paths.mjs:543
  if (!response.ok) throw new Error(`GitHub API ${response.status} for ${path}`);
                          ^
Error: GitHub API 404 for /repos/objectstack-ai/objectstack/pulls/99999999/files?per_page=100&page=1
    at async collect (.../check-single-claim-paths.mjs:502:16)
    at async .../check-single-claim-paths.mjs:901:66
Node.js v22.22.2
exit=1            == EXIT_CONFLICT: "an earlier open PR already claims a listed path"

After:

$ PR_NUMBER=99999999 GITHUB_REPOSITORY=objectstack-ai/objectstack node scripts/check-single-claim-paths.mjs
info  re-exec with --use-env-proxy: HTTPS_PROXY is set and node's fetch does not read it.
X check:single-claim-paths: PREREQUISITE NOT MET - the board was not read - GitHub API 404 for
  /repos/objectstack-ai/objectstack/pulls/99999999/files?per_page=100&page=1 - NOT a verdict,
  not a clean tree.

  Nothing below this line is a reading. This run never learned which paths any pull request
  claims, so it says nothing about whether one is claimed twice, and it accuses no author of
  anything. Exit 3 rather than 1: that code means an EARLIER open PR already claims a listed
  path, and a failed read is not evidence of any such PR. Exit 3 rather than 0 too - a board
  that could not be read is not a clean board.

  Usual causes, in the order worth checking: the token cannot read this repository, the PR
  number resolves to no pull request, or the request never left the container [...]
exit=3            == EXIT_PREREQUISITE_NOT_MET

Live reverse control, same tree, a PR number that really resolves — unchanged:

$ PR_NUMBER=18935 GITHUB_REPOSITORY=objectstack-ai/objectstack node scripts/check-single-claim-paths.mjs
OK check:single-claim-paths: PR #18935 modifies none of the 1 declared at-most-one-writer path(s),
   so there is nothing to serialise.
exit=0

Shape of the change:

  • The read only is wrapped. judge stays outside the try on purpose: it is pure and cannot
    throw on a transport, and a catch spanning it would relabel a real crash in the verdict layer
    as "the board was not read" — the same lie in the other direction.
  • The failure becomes a refusal through a pure handler, boardNotReadRefusal(error), returning
    { exit, lines } exactly as judge does. That is what lets the self-test drive the arm with
    no process exit and no network. The single process.exit stays at the dispatch, where every
    other verdict's exit already lives.
  • The code is EXIT_PREREQUISITE_NOT_MET, imported from the fleet's shared register, the way
    scripts/check-issue-citations.mjs and scripts/pm/post-stamped.mjs already spell it, so the
    wording family is one phrase across the tree: "PREREQUISITE NOT MET — the board was not read".
  • It stays non-zero. A board that could not be read is not a clean board, so the CI consumer
    (.github/workflows/single-claim-path-guard.yml) is still red on it. That is the right answer,
    and 0 would have been the anti-pattern the header names.
  • The header's exit register gains code 3 plus the paragraph saying why 1 could not be left to
    node's unhandled-rejection status.

The CI consumer needs no change, measured. Its only comment about an exit code, at
single-claim-path-guard.yml:94-96, names 2 for one specific condition (PR_NUMBER absent) and
does not enumerate the register, so there is nothing there to extend. The run: line is
unchanged and any non-zero exit is a red job.

B. The self-test handshake was set before the block that produces the verdict

selfTestReachedVerdict = true sat on the line directly above return (async () => { — the
block that runs every assertion, evaluates the battery floor and prints the verdict. The flag was
therefore true before a single case had run, so an early return or a throw anywhere inside that
block left it true and the dispatch's if (!selfTestReachedVerdict) branch was unreachable. The
one sentence the flag exists to make possible could never be said by this gate.

AGENTS.md, verbatim: "Set the flag as the self-test's last statement, after its success line
prints" and "SAY the self-test never reached its verdict. An exit code is not a handshake."

The assignment is now that last statement. The failing arm deliberately gets none: it calls
process.exit(1) on the next instruction, so no reader survives to consult the flag — the
handshake is for the SILENT ways out, which is exactly what the new position catches.

The negative control — red, then green, both legs on committed files

The instrument is scripts/ablation-replace.mjs, so the mutation and the restore are both proven
against the disk rather than against an exit code. The injected mutation is identical in both
legs: an early return; as the first statement inside the async verdict block.

Leg 1 — the fixed file (this branch's HEAD):

ablation-replace: anchor   x1 -> x0
ablation-replace: blob     8850395d7856f4299b96dd45b68916af6a0c9faa -> 8387e64acd8bacddc7a8b07b4cd991414edbe216
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 8850395d7856 -> 8387e64acd8b
ablation-replace: running: node scripts/check-single-claim-paths.mjs --self-test

X check-single-claim-paths 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
ablation-replace:   blob after restore  8850395d7856f4299b96dd45b68916af6a0c9faa
ablation-replace:   blob at HEAD        8850395d7856f4299b96dd45b68916af6a0c9faa
ablation-replace:   git diff HEAD       empty
ablation-replace: ok restored: blob == HEAD (8850395d7856) and `git diff HEAD` is empty

Leg 2 — the same injection on the pre-fix file (commit d4b1f2817, i.e. after A and before
B), restored into the tree with git restore --source=... --worktree so the index is untouched:

ablation-replace: anchor   x1 -> x0
ablation-replace: blob     2bedc9c313d74167c938a8c706adcfbae87a4b13 -> e5b5d1c042e6735f8e40eb874e3adcdca2f2429e
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 2bedc9c313d7 -> e5b5d1c042e6
ablation-replace: running: node scripts/check-single-claim-paths.mjs --self-test
ablation-replace: command exited 0
ablation-replace:   blob after restore  8850395d7856f4299b96dd45b68916af6a0c9faa
ablation-replace:   blob at HEAD        8850395d7856f4299b96dd45b68916af6a0c9faa
ablation-replace:   git diff HEAD       empty
ablation-replace: ok restored: blob == HEAD (8850395d7856) and `git diff HEAD` is empty

Leg 2 printed zero bytes from the self-test and exited 0. That is the defect, executed:
the runner had nothing to say, and "every case held" and "no case ran" were the same output. This
is why the triage asked for a RED negative control — without it, fixed and unfixed look identical
in the gate's own output on a healthy run.

git status --porcelain is empty after both legs, and the working blob equals the HEAD blob.

Self-test counts

batteries cases
base 26c73fb4e 8 74
this branch 9 93

SELF_TEST_BATTERY_FLOOR rises 8 to 9 with the roster, because leaving it at 8 would let the new
battery be deleted without a red — which is what the roster comment already says about deletion.
The measured base is 74 cases, not the 83 the dispatch carried; the 74 is also exactly the sum of
the eight declared floors, so the floors sat at the live counts before this change and still do.
The 19 new cases pin the code and the words, drive a throwing fake api through collect into the
same handler, keep a healthy api reaching a real verdict as the reverse control, and pin
structurally that the handler is reached after the LAST network read — the live one — so deleting
the live catch reds instead of being satisfied by the self-test's own calls to the handler.

No existing verdict moved: the CLEAN / CONFLICT / NOT WIRED pins are byte-identical and the three
older constants keep their values, asserted as a new case (the four codes are four distinct
values).

Census — who else carries this ordering bug

The card left this unmeasured and asked for it. Measured on this branch, and not fixed here.

Population: 105 files under scripts/ that both declare let selfTestReachedVerdict = false and
read it at the dispatch. Two independent mechanical criteria, which agree on the same 4
candidates: (1) a success line printed after the last flag assignment; (2) an assertion-registering
call (registerCase, batteryFloorFailures, checkSelfTestFloor) after the last flag assignment.
All 4 then read by hand, because the mechanical signal cannot tell "the verdict is decided at the
dispatch" apart from "assertions still run after the flag".

file flag line what follows the flag verdict
scripts/check-single-claim-paths.mjs 807 (pre-fix) the entire async block it returns, opened at 808: every assertion, the floor, the verdict print ORDERING BUG — fixed here
scripts/check-osv-exemptions.mjs 647 nothing inside selfTest(); the flag is its last statement. The floor call at 675 is at the DISPATCH, after the flag is read at 657 holds
scripts/typecheck-configs.mjs 350 same shape: flag last in selfTest(), floor at 377 in the dispatch, flag read at 357 holds
scripts/check-durability-degradation-log-level.mjs 5588 and 5592 nothing — set adjacent to each of its two verdict lines, green and red. The sinks at 6718 and 6851 belong to a SECOND self-test carrying its OWN flag, readSeamsReachedVerdict (5609) holds

The other 101 set the flag as the last statement of the function whose verdict it certifies.

So this is one card, not a class — which also answers the card's own question about whether
the B half should have been promoted to a census card. It should not.

Worth recording for the next reader: docs/audits/2026-09-self-test-shape-census.md scores this
very file HELD (its row, line 321). That is correct for the axis it measured — its probe
injects return; as the first statement of the dispatched function, which lands above the flag
assignment and therefore does leave the flag false. The ordering axis is a different injection
point, inside the block, and the published census is blind to it by construction. Neither reading
contradicts the other.

Changeset

skip-changeset, measured rather than assumed. Nothing published moves: 70 non-private packages
declare files[] and every one ships dist, README.md and CHANGELOG.md only — none ships
repo-root scripts/. The only reference to this script outside scripts/ is the private root
manifest's own check:single-claim-paths line. The positive control for the method is that same
scan finding 70 populated files[] arrays, so an empty result is a reading and not a silent miss.

Verification

  • pnpm check:single-claim-paths (the self-test) — pass, 93 cases, 9 batteries.
  • The 29 gate commands derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
    from this worktree, against the real change set (1 path, three-dot semantics vs merge base
    26c73fb4e). Every exit code captured redirect-then-$?, never through a pipe. All 29 exited 0, the slowest being
    pnpm check:pm-dispatch-gates at 745s (1848 self-test cases) under contention with a sibling
    worktree running the same gate. Reconciled with dispatch-gates.mjs --ran, each line recorded as
    command :: exit N: 29 derived, 29 run, 0 UNRUN, 0 NOT-MEASURED as a DERIVED zero rather than a
    claimed one, and none of the 29 exited 3.
  • Control-byte self-scan over the changed file (grep -naP over the C0 range plus DEL): no hits.
  • The derivation's own NOT MEASURED rows are left as it reports them, including the one for this
    very script's live invocation, whose argv takes a value only inside a CI run.

Reader test

A transport failure in CI reads PREREQUISITE NOT MET and a red job, never "an earlier PR claims
your path". A self-test that dies mid-block is reported as never reaching its verdict.

Acceptance notes

Observations from this file, noted and deliberately not filed and not fixed — none is a
reproducible defect, a declared-contract violation or a metadata-authoring trap:

  • The exit register's prose in the header is now four codes long and reads as a list; if a fifth
    ever arrives, the register would be better as a table. Style, no behaviour.
  • MAX_PAGES is 30 in both loops with one comment explaining the file-list ceiling; the open-PR
    loop borrows the same constant for a different endpoint's ceiling. Nothing observable follows
    from it today at this repo's open-PR count.

Generated by Claude Code

…o PREREQUISITE NOT MET instead of the accusation code

The live path read a PR's file list with nothing catching a throw, so any
transport failure — a 404 from a number that resolves to no pull request, a
dead credential, a dropped socket — exited on node's unhandled rejection
status, 1. This file defines 1 as "judged, an earlier open PR already claims
a listed path": an accusation about a board the run never read, against an
author who did nothing.

Its own header already forbade exactly that ("a mis-wired gate must not read
as an accusation, because it would be red on every PR at once for something
no author did"), so this is the file keeping its own rule.

The read — and only the read — is now wrapped. `judge` stays outside the try
on purpose: it is pure and cannot throw on a transport, and a catch spanning
it would relabel a real crash in the verdict layer as "the board was not
read". The failure is turned into a refusal by a pure handler that returns
`{ exit, lines }` exactly as `judge` does, so the self-test drives the arm
offline with no process exit and no network.

The code is EXIT_PREREQUISITE_NOT_MET, imported from the fleet's shared
register rather than numbered locally, the way check-issue-citations and
post-stamped already spell "the board was not read". It stays NON-ZERO: a
board that could not be read is not a clean board, so the CI consumer is
still red on it.

Probe, in an agent container, before and after
(PR_NUMBER=99999999 GITHUB_REPOSITORY=objectstack-ai/objectstack):
  before: unhandled rejection, stack trace, exit 1
  after:  "PREREQUISITE NOT MET — the board was not read — GitHub API 404
          for /repos/.../pulls/99999999/files... — not a verdict, not a
          clean tree", exit 3
Live reverse control, PR_NUMBER=18935: unchanged, clean verdict, exit 0.

Self-test: a new battery of 19 cases, roster floor raised 8 -> 9 batteries,
74 -> 93 cases. It pins the code and the words, drives a throwing fake api
through collect into the same handler, holds a healthy api reaching a real
verdict as the reverse control, and pins structurally that the handler is
reached after the LAST network read — the live one — so a deleted live catch
reds rather than leaving the pin satisfied by the self-test's own calls.

Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
… the verdict prints, not before the async block that produces it

`selfTestReachedVerdict = true` sat on the line directly above
`return (async () => {` — the block that runs every assertion, evaluates the
battery floor and prints the verdict. The flag was therefore true before a
single case had run, so an early return or a throw anywhere inside that block
left it true and the dispatch's `if (!selfTestReachedVerdict)` branch was
unreachable. The one sentence the flag exists to make possible — "selfTest()
returned without reaching its verdict" — could never be said by this gate.

AGENTS.md, verbatim: "Set the flag as the self-test's last statement, after
its success line prints" and "SAY the self-test never reached its verdict.
An exit code is not a handshake."

The assignment now is that last statement. The failing arm is left without
one deliberately: it calls process.exit(1) on the next instruction, so no
reader survives to consult the flag — the handshake is for the SILENT ways
out, which is what the new position catches.

Negative control (deliberate early return injected inside the block, on the
committed file, restored by blob hash): red, "selfTest() returned without
reaching its verdict", exit 1. Before this commit the identical injection
printed the same partial output and exited 0.

Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
@os-elon-musk os-elon-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 18, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 03310bc914f2f633f9d57c6b206bc0eae53b8b6d

① Derived judgments

  • Surface: scripts/check-single-claim-paths.mjs only (+160 / −5), two commits (A then B). Off the governed register; no published package moves; no contract's accept/reject set moves. No landing on the file since the claim; the proxy re-exec PR fix(scripts): route check:single-claim-paths through the session proxy so a seat can run it #18935 landed at 07:25Z is on the base and untouched.
  • A. The live board read is wrapped and any thrown read becomes boardNotReadRefusal(error) — a pure handler returning { exit: EXIT_PREREQUISITE_NOT_MET, lines } (the fleet's 3, imported from ./pm/check-half-states.mjs beside the proxy symbols), printed at the single dispatch site; judge stays outside the try. The text says 「the board was not read — — ⛔ not a verdict, not a clean tree」 and states why 3 rather than 1 (the accusation) and why 3 rather than 0; the header register gains the code. Seat probe on a scratch worktree at this head: PR_NUMBER=99999999 … ⇒ exit 3 with that line after the re-exec note (before, on main: an unhandled rejection, exit 1); the live control PR_NUMBER=18935 ⇒ exit 0 with the gate's clean verdict. The CI consumer needs no change (a non-zero exit is a red job; its only exit-code comment names 2 for one condition and enumerates nothing) — correct reading.
  • B. selfTestReachedVerdict = true moves from the line above return (async () => { to the last statement inside the block after the success line; the failing arm gets no assignment because it exits 1 on the next instruction. The negative control (an early return; injected as the first statement inside the block, through scripts/ablation-replace.mjs with the blob-hash restore): on the fixed head the runner prints 「returned without reaching its verdict」 and exits 1; on the pre-B commit the self-test printed zero bytes and exited 0 — the defect executed, then the fix shown to catch it. AGENTS.md :1039's contract now holds for this file.
  • Self-test: 74 cases / 8 batteries on the base (the dispatch quoted 83 / 8 — a number the seat relayed from the superseded PR fix(scripts): re-exec check:single-claim-paths through the session proxy so its verdict is readable outside CI #18945's head rather than measuring on main; the seat's correction) → 93 / 9 on this head, floor raised with the roster; the new battery pins the handler's exit, its text, and the integration shape (a throwing api escaping collect).
  • The census the card named, measured by the dev and read by the seat: 105 files carry the three-piece; two mechanical criteria and a hand read find ONE file with the ordering bug — this one — so B is not a class and no census card is owed. Correct disposition.
  • Seat spot-check: check-self-test-wired, check-scripts-symbol-anchors, check:entry-guard, check:parse-guard exit 0 on the head.

② Semver level

  • skip-changeset is correct: root scripts/ ships in no package's files[] (the dev's 70-package scan with a positive control). Clause-②: no on the claim (5727821701) and in the body; PM_SWEEP_REPO=objectstack-ai/objectstack node scripts/pm/check-clause2-carriers.mjs --pair 18986 at 2026-09-18T09:49Z: exit 0, one live claim, no widening tell.

③ Boundary flags

  • Dev flags: no open questions; one to file the dev left to the seat's judgment — scripts/measure-self-test-floor.mjs's probe injects return; as the FIRST statement of the dispatched function, which for this file sat ABOVE the flag assignment, so the 2026-09 self-test shape census scored this file HELD while its dispatch diagnostic was unreachable for every early exit inside the async block. The seat judges it class (a): a reproducible false clearance in an instrument AGENTS.md sends authors to; filed by the seat (number in the ACCEPT). Three noted, not filed (the register as prose; the shared MAX_PAGES; the live judgment running in the advisory workflow, already in the gate census's report-only worklist) — agreed. Deviation declared: the floor count disagreement, above. Gates: 29 derived / 29 run / 0 NOT-MEASURED at this head; check:pm-dispatch-gates 745 s detached with its exit captured. 0 MCP calls; 3 REST writes.
  • CI on this head at 2026-09-18T09:49Z: 16 success · 11 skipped · 6 in_progress — nothing red; the landing act reads it again. ⚠️ The landing channel is closed (this session's auto-mode classifier, record 5726459594 on PR fix(pm): a size refusal is a refusal — post-stamped stops reporting a real 422 as a missing route #18934) — this PR joins the four waiting for it.

Implemented-by: claude/issue-18940-single-claim-paths-contracts
Reviewed-by: session_01BTeBejoPUvRHN8WdAJC6oF

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Landing record — every pre-check met, blocked at the channel (skills seat, session_01BTeBejoPUvRHN8WdAJC6oF) · 2026-09-18T10:12Z

Every check on 03310bc914 completed with none red (35 runs: 24 success, 11 rostered skips; Lint & Repo Gates completed at 2026-09-18T10:10Z, read at 2026-09-18T10:11Z); the review of record is 5728290255 (PASS); --pair 18986 exit 0; ACCEPT 5728294955 on #18940, with its correction 5728309827 (the filed card is #18987). One file, scripts/check-single-claim-paths.mjs, and no other open draft on it. The landing act (POST …/pulls/18986/ccr/ready_for_review then PUT …/pulls/18986/ccr/auto_merge {"merge_method":"SQUASH"}) is not attempted here: the same act was refused by this session's auto-mode classifier on PR #18934 at 2026-09-18T07:03Z (record 5726459594 there) and a classifier refusal is 「no channel」 per references/rest-channel.md. The PR stays draft, ACCEPTED, and lands the moment a channel exists — this seat's once the maintainer allows the two CCR calls, or the maintainer's own hand. ⛔ Nothing here is a finding against the PR.


Generated by Claude Code

os-elon-musk pushed a commit that referenced this pull request Sep 18, 2026
…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>

Copy link
Copy Markdown
Collaborator Author

Landing attempt on the maintainer's word — refused a fourth time, with the allow rule on disk (skills seat, session_01BTeBejoPUvRHN8WdAJC6oF) · 2026-09-18T14:25Z

PR #19047 (.claude/settings.json +3 / −1: the two landing-call allow rules, Bash(curl -sS -X POST https://api.github.com/repos/objectstack-ai/objectstack/pulls/*/ccr/ready_for_review *) and the PUT …/ccr/auto_merge twin) merged at 2026-09-18T14:20Z (67624b78d, os-zhuang). The seat fast-forwarded its checkout to that commit (rules present at :51–:52 on disk), re-ran every pre-check on this PR at 2026-09-18T14:23Z — NOT governed, --pair 18986 exit 0, record 5728290255 on the head 03310bc914, CI 22 success · 9 rostered skips · 0 red, mergeable_state clean — and typed the landing act in the rules' literal-prefix shape: curl -sS -X POST https://api.github.com/repos/objectstack-ai/objectstack/pulls/18986/ccr/ready_for_review -H "Content-Type: application/json" -d '{}'. The session's auto-mode classifier refused it at 2026-09-18T14:24Z with the reason 「Merge Without Review」 — the same refusal as the three earlier ones today, which means no allow rule matched: this session's permission set was loaded when the session started and does not carry the two rules committed since. A classifier refusal is 「no channel」 (references/rest-channel.md :40); the seat does not re-issue the act by another route.

⇒ The rules are right and on main; what is missing is a session that loaded them. Either a fresh skills-seat session (its first act: this exact command on this PR) lands the eight ACCEPTED PRs, or the approver's hand does. Nothing here is a finding against the PR; it stays draft, ACCEPTED, every pre-check met.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Landing record — landed on the seat's record through the CCR route (skills seat, session_01BTeBejoPUvRHN8WdAJC6oF) · 2026-09-18T14:29Z

Channel: the two allow rules of PR #19047 (67624b78d, merged 2026-09-18T14:20Z) on disk, and the session's permission mode switched by the maintainer from auto to manual confirmation at 2026-09-18T14:25Z (the auto-mode classifier had refused the same act four times, the last at 14:24Z with the rules already on disk); the act typed in the rules' literal-prefix shape. Pre-checks re-run at 2026-09-18T14:23Z: NOT governed (scripts/check-single-claim-paths.mjs); --pair 18986 exit 0; the review of record is 5728290255 (PASS) on the head 03310bc914; CI on the head 22 success · 9 rostered skips · 0 red; mergeable_state clean; ACCEPT on #18940.

Acts: POST …/pulls/18986/ccr/ready_for_review HTTP 200 at 2026-09-18T14:26Z (timeline ready_for_review, draft: false read back); PUT …/pulls/18986/ccr/auto_merge {"merge_method":"SQUASH"} HTTP 200 at 2026-09-18T14:27Z (timeline auto_merge_enabled; the repository allows squash only — allow_squash_merge true, allow_merge_commit false — so the queue's merge is a squash whatever the response's merge_method field prints); the queue took it — timeline added_to_merge_queue at 2026-09-18T14:27Z. The queue rebuilds the PR onto the current main and re-runs the subscribing workflows; the merge is the queue's, ⛔ not this seat's click. Residue on the merge: #18940 → pm:done, unassigned.


Generated by Claude Code

Merged via the queue into main with commit 7af6983 Sep 18, 2026
37 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-18940-single-claim-paths-contracts branch September 18, 2026 14:46
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…o a flag above the returned block is no longer scored HELD (objectstack-ai#19026)

Fixes objectstack-ai#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
objectstack-ai#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. objectstack-ai#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 objectstack-ai#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 objectstack-ai#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>
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