You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 84a7e9e
Browse filesBrowse the repository at this point in the historyBrowse files
fix(pm): the clause-2 correction note READS the claim comment's two stamps instead of vouching it was never edited (#19298)
Fixes#18892
Clause-②: no
The `--pair` green line and the `C2-CORRECTION` note under it both
testified that the claim comment 「is NOT edited and still reads as it
was written」. Nothing in this file had ever read an edit. `created_at`
and `updated_at` both arrive on the `/issues/comments` row
`applicableCorrection` already holds, so the reading cost no request —
it simply was not taken.
Triage's grading (5725446014) chose option **A** over the card's B and
C: read the two fields, say it in the message, ⛔ never fail on it.
## What changed — `scripts/pm/check-clause2-carriers.mjs` only
- **`claimEditReading(row)`** — module-local, ⛔ no new `export` and no
registration (Clause-② declared `no`). It turns the claim comment's own
two stamps into **three** readings, never two:
- `updated_at` differs from `created_at` ⇒ **WAS EDITED**, naming the
instant so a reader can open that comment's edit history;
- the two are equal ⇒ a **measured UNEDITED**, naming both stamps it
compared;
- a row carrying neither stamp ⇒ **NOT READ**, said as a gap — ⛔ never
as "unedited", which is the sentence this card was filed against.
- **The correction note states that reading** in place of the assertion.
**Report-only**, per the ruling: no exit moves, and an edited claim
still reads `declared` at the seat's own value. An edit is a legitimate
act; the silence was the defect.
- **The green line's parenthetical** now describes what the reading
above actually states, and names the stream that reading prints on.
## The pin was proven red BEFORE the fix existed
Two commits, in that order, exactly as the card required (「⛔ 先让 pin 红」).
`0d29892` — the pin alone, against the unfixed message:
```
$ node scripts/pm/check-clause2-carriers.mjs --self-test # exit 1
✗ ⭐ the EDITED specimen is REPORTED in the note, in as many words
✗ …naming the edit instant beside the creation one, so a reader can open that comment's history
✗ ⭐ an UNEDITED claim reads as a MEASURED unedited, naming both stamps it compared
✗ ⭐ a row carrying NO `updated_at` reads NOT READ — ⛔ never "unedited", which is this defect one room over
✗ ⭐ the three readings are three DIFFERENT sentences — one sentence for all three was the defect
✗ ⭐ the green line points at that reading and says what it STATES, ⛔ never testifying to the edit itself
✗ ⛔ …and the clause that asserted an unread fact is gone from the printed line
✗ ⛔ NEGATIVE, on the SOURCE: no branch of this file asserts the unread 「NOT edited」 any more
✗ ⛔ CONTROL — the same source read is not empty or misdirected: it reaches the reader this card added
✗ check-clause2-carriers self-test: 9 of 1071 case(s) failed.
```
`0128cbb` — the reader:
```
$ node scripts/pm/check-clause2-carriers.mjs --self-test # exit 0
✓ check-clause2-carriers self-test: 1071 cases pass (…)
```
The unedited control and the three report-only cases pass on **both**
commits, so the battery is not a one-sided pin. The source-scan negative
and its control are assembled at runtime (the #16770 idiom), because a
literal would have made the scan hit itself — the first draft did
exactly that and passed vacuously; it is fixed and the control now
proves the read reaches the file.
## Measured on the specimen the card names — offline, via `--pair-json`
objectui's claim `5724909959` on card #9764: `created_at`
`2026-09-18T03:54:37Z`, `updated_at` `2026-09-18T04:23:23Z`. Replayed
with the real API payloads, thread truncated to the instant the card
measured:
**Before** (`adf4b18`):
```
ℹ️ C2-CORRECTION — … And it supersedes claim comment 5724909959's own declaration,
which is NOT edited and still reads as it was written.
```
**After** (`0128cbb`):
```
ℹ️ C2-CORRECTION — … And it supersedes claim comment 5724909959's own declaration,
and ⚠️ it WAS EDITED at `2026-09-18T04:23:23Z` (`created_at` `2026-09-18T03:54:37Z`)
— REPORTED and ⛔ never a failure: read its edit history before taking the
declaration under it for the one the seat first wrote.
```
And the sharper half — two `--pair-json` documents differing **only** in
`updated_at`, both reaching exit 0:
| | before | after |
|---|---|---|
| edited claim | exit 0, note says `NOT edited` | exit 0, note says `WAS
EDITED at …` |
| unedited claim | exit 0, note says `NOT edited` | exit 0, note says
`reads UNEDITED — … both …` |
Before this PR the two runs were **byte-identical on every stream**.
That identity is now impossible, and the exit register did not move in
either row.
## Acceptance notes
- **The PM's mechanism assumption 3 is REFUTED, and measured so.** The
pass path does **not** print three lines with no per-item readings:
`renderPair`'s `rows.length === 0` branch prints every note (`ℹ️
C2-CORRECTION — …`) immediately above the green line. So 「the reading
above」 resolves today and was ⛔ not dropped. What is true is that the
**note goes to stderr while the green line goes to stdout**, so a seat
capturing stdout alone holds the pointer without its referent — which is
the likeliest reading of the card's 「只打三行」. The parenthetical now names
the stream, which is the cheapest thing that makes the pointer
executable. No reading was deleted.
- **A `--pair-json` document that omits `updated_at` changes reading**,
from a silent "unedited" to an explicit NOT READ. That is the intended
direction (absence must be loud) and affects hand-written fixture
documents only; the live `/issues/comments` rows always carry both
fields, unprojected — `readCardComments` hands the raw rows through.
- **noted, not filed** — the *plain* branch of the green line
(declaration read from the claim comment itself) states nothing about
editing, so nothing false lives there and no reading was added to it. A
claim comment could still have been edited into carrying its own
declaration; the gate says nothing about that in either direction today.
Extending the reading to that branch is a capability decision this card
did not rule on, and adding it would have meant a new note on every
corrected-free pair. Succeeding reader: whoever takes the escalation
condition triage recorded on this card (a pair whose claim was edited
*into* carrying the declaration ⇒ p1).
- **noted, not filed** — the shallow checkout (`git rev-parse
--is-shallow-repository` ⇒ `true`, 116 commits; this file reads as
`+9132` insertions at the boundary commit) makes `git log -S` unable to
answer whether the pass path ever failed to print its notes. Stated
rather than answered; nothing in this PR rests on it.
- **Net +39 lines** (9,925 → 9,964) against the card's +40 ceiling. One
file. No changeset: `scripts/pm/**` publishes nothing, hence
`skip-changeset`.
- `scripts/pm/**` is not a governed path, so no 维护者速读 section is owed.
## Gates
Derived from the worktree after the last commit, no paths passed:
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `0128cbb` ⇒ **34 commands**, matching the
dispatch-time derivation. Each is run with its exit code captured
**before** any pipe; the full table, its `--ran` reconciliation and the
head sha it was taken on are in this card's `os-dev-report` comment,
which is the machine-read record.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
'#18536: the lane-keyed owed population — spec and skills owe the record on EVERY round, other lanes owe none, a `yes` outside them is spec-lane work': 30,
839
839
'#18862: cross-author LIVE claims with no `Release:` between — the hand-over the protocol never wrote, named; judged only after its effective instant': 52,
840
840
'#16770: the exit-0 line says which carriers agreed — LABEL carriers — and that the PR body was not read': 14,
841
+
'#18892: the claim comment\'s EDIT reading — taken from the two stamps already in hand, reported and never failed': 10,
841
842
});
842
843
843
844
// DELETING an entry silences that battery's floor exactly as effectively as
// by the one #16833 adds, and by the one #18456 adds, and by the one #18719
852
853
// adds, and by the one #18683 adds, and by the one #18764 adds, and by the one
853
854
// #18828 adds, and by the one #18536 adds, and by the one #18862 adds, and by
854
-
// the one #16770 adds.
855
-
const SELF_TEST_BATTERY_FLOOR = 35;
855
+
// the one #16770 adds, and by the one #18892 adds.
856
+
const SELF_TEST_BATTERY_FLOOR = 36;
856
857
857
858
// The key an assertion is filed under when no battery is open. It is not a
858
859
// declared battery, so it reds by the same set difference rather than silently
@@ -1458,6 +1459,20 @@ function newestFirst(a, b) {
1458
1459
return b.index - a.index;
1459
1460
}
1460
1461
1462
+
/**
1463
+
* The claim comment's own EDIT reading, from the two stamps ALREADY on the row
1464
+
* this reader holds — ⛔ never asserted, which is what #18892 measured: the note
1465
+
* below testified 「NOT edited」 about objectui#9764's claim 5724909959, whose
1466
+
* `updated_at` is 29 min past its `created_at`. THREE readings, ⛔ never two — a
1467
+
* missing stamp is a GAP, ⛔ not an unedited; ⛔ report-only per that ruling, an
1468
+
* edit is legitimate and moves no exit. The untaken testimony was the defect. */
1469
+
function claimEditReading(row) {
1470
+
const [c, u] = [row?.created_at, row?.updated_at].map((v) => (typeof v === 'string' ? v : null));
1471
+
if (c === null || u === null) return 'whether it has been EDITED is NOT READ — its row carries no `created_at`/`updated_at` pair to compare, so ⛔ read that as a gap and never as "unedited"';
1472
+
if (c === u) return `it reads UNEDITED — its \`created_at\` and \`updated_at\` are both \`${c}\``;
1473
+
return `⚠️ it WAS EDITED at \`${u}\` (\`created_at\` \`${c}\`) — REPORTED and ⛔ never a failure: read its edit history before taking the declaration under it for the one the seat first wrote`;
1474
+
}
1475
+
1461
1476
/**
1462
1477
* Which correction, if any, governs this card's declaration — and when none
1463
1478
* does, WHY, in a sentence that names an act the claiming seat can perform.
@@ -1467,8 +1482,8 @@ function newestFirst(a, b) {
1467
1482
* here that a seat cannot answer would rebuild that door one room over, so the
@@ -8027,7 +8044,7 @@ export async function selfTest() {
8027
8044
t('⛔ …so the governing claim does not move, and the card is not re-claimed', governingClaim(REPAIRED_THREAD)?.createdAt === governingClaim(BROKEN_THREAD)?.createdAt);
8028
8045
t('⛔ nor is the correction read as a MISPLACED declaration — it is the designated second carrier', cardDeclaration(REPAIRED_THREAD).state !== 'misplaced');
8029
8046
t('the reading PRINTS: a note names the correction rather than answering 0 in silence', typeof c2CorrectionNote(repairedPair) === 'string');
8030
-
t('…and says the claim comment was NOT edited', says(c2CorrectionNote(repairedPair), 'NOT edited'));
8047
+
t('…and STATES the claim comment\'s edit reading, measured from its own two stamps (#18892)', says(c2CorrectionNote(repairedPair), 'UNEDITED'));
8031
8048
t('…and names the comment id it corrects, so a reader can find it', says(c2CorrectionNote(repairedPair), String(C_CLAIM_ID)));
8032
8049
t('…and states the attribution ceiling rather than claiming a verification', says(c2CorrectionNote(repairedPair), 'DECLARED identity, never a verified one'));
8033
8050
t('…and it rides as a NOTE, never as a finding — pairNotes carries it, pairRows does not', pairNotes(repairedPair).some((n) => n.code === 'C2-CORRECTION') && pairRows(repairedPair).every((r) => r.code !== 'C2-CORRECTION'));
@@ -9422,6 +9439,28 @@ export async function selfTest() {
9422
9439
t('⛔ CONTROL: the label the line names is the constant C1 compares, ⛔ not a second spelling of it', says(LABEL_CARRIERS_AGREE, CONTRACT_REVIEW_LABEL) && CONTRACT_REVIEW_LABEL === 'needs:contract-review');
9423
9440
t('the pair is still identified in the line, in the spelling the round reports paste', says(G_BRANCHES[0][1], '✓ check-clause2-carriers: PR #16761 / card #16568 — the clause-② declaration is readable in the fixed spelling'));
9424
9441
9442
+
// -- #18892: the EDIT reading, taken rather than asserted. ⭐ Measured specimen:
9443
+
// objectui#9764's claim 5724909959 was EDITED (`created_at` 2026-09-18T03:54:37Z
9444
+
// vs `updated_at` 04:23:23Z) and the note testified 「NOT edited」 anyway. The
9445
+
// retired sentence is ASSEMBLED below, or the source pin hits itself (#16770).
9446
+
battery('#18892: the claim comment\'s EDIT reading — taken from the two stamps already in hand, reported and never failed');
t('⭐ the EDITED specimen is REPORTED in the note, in as many words', says(E_NOTE(E_EDITED), 'WAS EDITED'));
9453
+
t('…naming the edit instant beside the creation one, so a reader can open that comment\'s history', says(E_NOTE(E_EDITED), '2026-09-18T04:23:23Z') && says(E_NOTE(E_EDITED), '2026-09-12T00:41:08Z'));
9454
+
t('⭐ an UNEDITED claim reads as a MEASURED unedited, naming both stamps it compared', says(E_NOTE(E_UNEDITED), 'UNEDITED') && says(E_NOTE(E_UNEDITED), 'created_at') && says(E_NOTE(E_UNEDITED), 'updated_at'));
9455
+
t('⭐ a row carrying NO `updated_at` reads NOT READ — ⛔ never "unedited", which is this defect one room over', says(E_NOTE(E_NOSTAMP), 'NOT READ') && says(E_NOTE(E_NOSTAMP), 'never as "unedited"'));
9456
+
t('⭐ the three readings are three DIFFERENT sentences — one sentence for all three was the defect', new Set([E_NOTE(E_EDITED), E_NOTE(E_UNEDITED), E_NOTE(E_NOSTAMP)]).size === 3);
9457
+
t('⛔ REPORT-ONLY: an edited claim raises NO C2 finding, so the exit register does not move', pairRows(pair({ cardComments: E_EDITED })).every((r) => r.code !== 'C2'));
9458
+
t('⛔ …and reads DECLARED at the seat\'s own value, exactly as the unedited thread does', cardDeclaration(E_EDITED).state === cardDeclaration(E_UNEDITED).state && cardDeclaration(E_EDITED).value === 'no');
9459
+
t('⭐ the green line points at that reading and says what it STATES, ⛔ never testifying to the edit itself', says(greenPairLine({ pr: 1, card: 2, corrected: true }), '`created_at`/`updated_at`'));
9460
+
t('⛔ …and the clause that asserted an unread fact is gone from the printed line', !says(greenPairLine({ pr: 1, card: 2, corrected: true }), 'says the claim was not edited'));
9461
+
t('⛔ NEGATIVE, on the SOURCE: no branch of this file asserts the unread 「NOT edited」 any more', !E_SRC.includes(['still reads as', 'it was written'].join(' ')));
9462
+
t('⛔ CONTROL — the same source read is not empty or misdirected: it reaches the reader this card added', E_SRC.includes(['function claimEdit', 'Reading('].join('')));
9463
+
9425
9464
// -- The floor: every declared battery RAN, and ran its cases (#13489) -----
9426
9465
//
9427
9466
// Evaluated after every battery has had its chance and BEFORE the verdict, so
0 commit comments