fix(pm): the clause-② correction remedy names THIS card's claim comment, never a specimen from another card - #18448
Conversation
…nt, never a specimen from another card Part (3) of the C2 remedy carried a LITERAL comment id — 5642248126, the #17366 specimen, which lives on card #17366 — so every C2 row on every card printed a real `Claim:` comment id belonging to a different card, inside a verdict about this one, and nothing in the output told a reader that. Nothing SELECTED that comment. A constant makes no selection, so a guard over the selection could never have fired; the cut is at the ids this file may PRINT. `correctionTarget` resolves them from the same pool `cardDeclaration` built its readings from, checks each against the card under test through the row's own `issue_url`, and withholds — loudly — any id it cannot show belongs to that card. Absence of `issue_url` is not read as a mismatch; only a positive disagreement is, and a reading with no card number names no id at all. No state, row or exit code moves: what changes is which digits part (3) carries, and whether it carries any. The self-test case that used to pin the foreign literal as the remedy's content is replaced by the property, and a new battery reds without the guard. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Contract review87/87 at-tier stamps (control: an ① Derived judgmentsScope of the diff: one file, scripts/pm/check-clause2-carriers.mjs, blob beed7f7bab to fe6601d55f, +257/-18, no other file at 2620cb2 differs from merge-base 55fd5ee; origin/main (read 2026-09-16T11:37:56Z as 66abef3) still carries beed7f7bab for this file, so base-vs-head here is also main-vs-head. Defect as actually found — CONFIRMED, and the card's offered shape could never have fired. On origin/main the id 5642248126 is a literal inside the CORRECTION_REMEDY string (line 1657), printed by every C2 row that carries the remedy, on every card. Nothing selected it. Card comments are read from /issues/{card}/comments (line 4143) and the card number comes from the PR body/branch, so on the live path every row already belongs to the card and a guard on the selected comment's issue_url has no input to fire on. The comment lookup the card relied on is re-verified: 5642248126 resolves to #17366 (claude[bot], "Claim: PM loop round 1"); control 5650083758 resolves to #17425. The round's account is correct; the card's mechanism attribution was wrong; the card's observation stands and is re-observable on any card via --pair-json. Direction A — nothing correct is silenced. The remedy is printed by exactly five states in c2Sentence (misplaced, malformed, missing, missing/describing, missing/inline-key); absent prints none. Each of the five was rendered on both copies from synthetic --pair-json documents (11:36:57Z): a C2 row prints on both, exit 4 on both, and the whole word-diff of the full output is 5642248126 replaced by the card's own governing claim id plus the clause "read from card #N's own thread". absent, a declared-with-yes pair (exit 4 via C3) and a declared-with-no pair (exit 0) are byte-identical across copies. Direction B — no exit code moves. Live, 19 open PRs, base then head per PR (11:34:34Z to 11:35:58Z): 17 pairs exit 0, 18420 and 17076 exit 2, identical 19/19 verdict-by-verdict; the only output delta is the x-ratelimit-remaining counter. Zero C2 rows printed live on either copy (control: 12 C2 rows on the synthetic set), so no live pair is on the printing path today and the live sweep exercises none of the changed lines; the synthetic set carries the accept/refuse comparison. 15 synthetic pairs: exits equal on both copies in every case (4/4, 0/0, 2/2). Fail-closed — correct, and absence is not conflated with disagreement. Rows without an id: no digits printed, sentence "found no claim comment id on card #N's own thread". Rows with an id but no issue_url: the id is printed (a row that states nothing contradicts nothing). A row declaring another parent: the id is withheld and the drop is printed with comment id and declared issue. Two ids in the pool at one timestamp: neither is printed, both are listed. Superseded plus governing claim: the governing id is printed. No card number (direct call): no id, "carries no card number". The printable pool is the same pool applicableCorrection accepts a correction from, so the remedy can only ever name an id that would land. Self-test — the pre-existing pin is real and the new battery bites. Base line 4452 pinned the literal as expected content, which is why 689 cases stayed green over the defect. Head replaces it with a property (key present, no foreign digits) and adds battery "#17919 ..." (25 cases, floor 24; roster floor 24 to 25; 689 + 25 + 1 = 715). Base 689 pass exit 0; head 715 pass exit 0. Ablation with part (3) restored to the literal: exit 1, 10 red, the sweep case naming all five printing states. Ablation with the foreign guard removed: exit 1 (by a TypeError at the battery's own foreign[0].card read; with that one access made optional, 4 red). Ablation loosening the fail-closed id rule: exit 1, 3 red. Direction predicted and observed in all three: the battery would not be green without the fix. Accept/refuse changes, one by one: (1) SELF_TEST_BATTERIES gains one entry, floor 24 to 25 — self-test only, tightens what a green self-test means. (2) commentCardNumber, correctionTarget — new pure exports of a repo-root script; no verdict reads them. (3) cardDeclaration takes an optional card and its withNote results carry a correctionTarget field — state, value and detail unchanged (asserted by the battery and confirmed by identical exits); no consumer serializes or deep-compares the object (0 hits, control 36 JSON.stringify sites). (4) CORRECTION_REMEDY constant becomes correctionRemedy(target) plus correctionTargetNote — text inside C2 rows only. (5) c2DeclarationUnreadable passes pair.card. Net: the gate accepts and refuses exactly what it did; only the digits in part (3) of the remedy, and a stated reason when there are none, change. Clause-②: no HOLDS. scripts/pm/ is under the private root package (@objectstack/spec-monorepo, private true); no package.json in the tree names scripts/pm (0 hits; control: 70 package.json files declare files[]); the two new exports are reachable only by importing the script by path. Nothing reaches a published package. Unexplained and NOT claimed by this fix — the 0 / 4 / 0 reading. The evidence-upgrade comment records the same command on the same pair answering 0, 4, 0 from two working directories with an identical script blob. This diff touches nothing in the fetch, the claim ordering, the repo resolution (env-driven, no cwd read) or the read-path selection, so it does not bear on that reading at all, and the reading remains open. I re-read #17425's 16 comments: every one has created_at equal to updated_at, so comment edits are excluded as the cause. The PR body's "without reproducing the non-determinism" is accurate; closing #17919 must not be read as settling the non-determinism. ② Semver levelskip-changeset, no changeset file — CORRECT. The rule: .claude/agents/os-dev.md lines 296-297 (the only criterion is that nothing published moves, published meaning what each package's files[] ships; fast path names scripts/pm/** as non-publishing), AGENTS.md lines 1064-1067 (the label is for a diff that publishes nothing from any released package), .github/workflows/pr-automation.yml lines 691-760 (route 2), .claude/skills/pm-dispatch/references/lanes/devx.md line 21 (root scripts/ implies skip-changeset), references/review-checklist.md line 20 (this repo takes the label, never an empty changeset). Verified: root package private, scripts/pm in no files[], labels read 11:33:00Z are size/m and skip-changeset, Check Changeset check run success on the head sha. ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Provenance — at-tier PASS, landing pre-check clear
Landing pre-check, all three: ① the PASS above names this head. ②
|
Fixes #17919
Clause-②: no
A CLI gate script exports no published symbol; the two new exports
(
commentCardNumber,correctionTarget) live inscripts/pm/, which nopackage's
files[]ships. The seat declared this on the card (claim comment5696260545) and the diff did not move it.The finding, and what it actually was
check-clause2-carriers --pairreported the clause-② declaration MISPLACED and,inside that message, named claim comment
5642248126— aClaim:on card#17366, unrelated to the card under test.
The card reads that as the script's selection. It is not. Part (3) of the
C2 remedy was a module-level constant that carried the id as a literal:
That is the #17366 specimen, hard-coded into the sentence every C2 row prints.
Nothing selected it, so no guard over a selection could have fired — a constant
makes no selection. The id printed on every card, in every C2 state that
carries the remedy, and had done so since the constant was written.
Verified on the exact blob the 2026-09-13 readings were taken from —
git cat-file -p aecbb2d86683eb908468fdacaac2ff53753f06ef | grep -n 5642248126→ line 1445, inside
CORRECTION_REMEDY. Same literal, same sentence.Reproduced deterministically, offline, on a card of its own
--pair-jsonjudges a pre-fetched pair with no network, so the reading isexactly reproducible. A synthetic card #99002 whose whole thread is comments
900000001and900000002, judged by theorigin/maincopy of the script:An id that is on neither comment of the thread it just read, printed inside the
verdict about that thread. ⇒ the class is re-observable today, on any card,
without reproducing the non-determinism the evidence upgrade measured.
The cut, and why it is here rather than where the card offered it
The card offers a guard that the selected comment's
issue_urlmatches the cardunder test, explicitly as a shape. Measurement moved it one level up: the only
ids this file may print are ids it read from the pool the reading was
built from, and each is checked against the card under test before it is
printed.
commentCardNumber(row)— the issue a row says it belongs to, read offissue_url; this is the card's own control, mechanised.correctionTarget(pool, card)— resolves the printable id from the SAME poolcardDeclarationbuilt its readings from. A positive disagreement drops theid and says so; ⛔ absence of
issue_urlis not read as a mismatch (anoffline document and this file's own fixtures carry rows without it, and a row
that states nothing contradicts nothing); a reading with no card number names
no id at all — fail-closed.
CORRECTION_REMEDYis a function of the card under test. When no id survives,part (3) names the key and no digits, plus the reason.
⛔ Nothing here resolves a state, a row or an exit code.
Both directions, judged — this is a gate other PRs must pass
Does the guard suppress a message that was correct? No. Word-diff of the
whole output, same input,
origin/maincopy vs this one:That is the entire delta. The MISPLACED verdict, its sentence, its remedy and
exit=4are byte-identical. A self-test case asserts each of the five remedy-carrying states still renders a non-empty row, so the controls cannot pass
vacuously.
Does any exit code move on a pair judged correctly? No. All 16 open PRs,
both copies run back to back, 2026-09-16T11:03–11:04Z:
Identical on both copies, 16/16, including all three non-zero verdicts.
Population — previously unmeasured, now partly measured
misplaced,malformed, and all threemissingvariants.absentdoes not print it (noclaim comment exists to correct). Enumerated mechanically: the ablation's own
failure detail names all five.
derived from 16 open PRs —
0with NO CLAIM COMMENT,0with a claim commentbut NO DECLARATION LINE ⇒ 0 pairs on the printing path today. The class is
a failure-path constant, not a standing one.
Tests
--self-test, the safety net, on2620cb2b3d: 715 cases pass, exit 0(689 before; +26). New battery
#17919: the correction remedy names THIS card's claim comment, never another card's, floor raised by exactly one.Ablation — the guard removed, part (3) restored to the pre-fix literal, on a
committed tree, mutation proved on disk before the run and the restore proved by
git diff HEADand a blob-hash comparison:Direction predicted before the run and observed: turns red. The module is
run directly from source by
node scripts/pm/…— there is no build and nodist/between the edit and the run, so the on-disk proof is the wholepreflight.
Gates: all 36 commands derived by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackrun at2620cb2b3d, exit 0 —including
pnpm check:pm-clause2-carriers,pnpm check:nul-bytesandpnpm check:pm-dispatch-gates(496.6s; a first run capped at 240s readexit=124, NOT MEASURED, and was re-run to completion). Reconciled:dispatch-gates --ran⇒ 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN.No changeset: the root package is private and
scripts/pm/sits outside everyworkspace package, so no
files[]can ship it.Acceptance notes
Out of scope, noted and ⛔ not filed:
misplacedsentence says "NOT in the card's claim comment" on threads thathave no claim comment at all (
misplacedandmalformedare bothreachable from the thread-wide scan with an empty claim pool). Prose
imprecision on a reachable path, not a wrong verdict; the remedy now degrades
correctly there. Carrier: whoever next edits
c2Sentence.the 条款②声明载体是一扇单向门:席位能把自己写进一个自己出不去的状态 —— 一个会话里同一个坑被踩了 5 次 #17366 specimen id. It is a doc comment inside a paragraph that is
explicitly about 条款②声明载体是一扇单向门:席位能把自己写进一个自己出不去的状态 —— 一个会话里同一个坑被踩了 5 次 #17366, never printed, and left as written.
Generated by Claude Code