Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 166 additions & 12 deletions scripts/pm/check-clause2-carriers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,7 @@ import {
isGateSemanticLabel,
labelNames,
latestMarkedComment,
markerMatches,
prDeliversCard,
proxyRearmPlan,
resolveSweepRepo,
Expand Down Expand Up @@ -781,6 +782,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
'#18701: ONE thread set -- what the template STATES is what the queue guard READS': 16,
'#18719: a RETRACTED claim leaves the pool — a withdrawn claim never governs': 36,
'#18683: the card-comment read pages to a cap — past 100 is UNJUDGED, ⛔ never a truncated pool': 27,
'#18764: a DECORATED claim ENTERS the pool — ONE reading, and it is the sibling\'s': 24,
});

// DELETING an entry silences that battery's floor exactly as effectively as
Expand All @@ -792,8 +794,8 @@ const SELF_TEST_BATTERIES = Object.freeze({
// one #17915 adds, by the one #17959 adds, by the one #18042 adds, and by the
// one #18174 adds, and by the one #18141 adds, and by the one #17919 adds, and
// by the one #16833 adds, and by the one #18456 adds, and by the one #18719
// adds, and by the one #18683 adds.
const SELF_TEST_BATTERY_FLOOR = 30;
// adds, and by the one #18683 adds, and by the one #18764 adds.
const SELF_TEST_BATTERY_FLOOR = 31;

// The key an assertion is filed under when no battery is open. It is not a
// declared battery, so it reds by the same set difference rather than silently
Expand Down Expand Up @@ -1756,7 +1758,7 @@ export function claimRetractions(commentRows) {
});
const out = new Map();
for (const claim of indexed) {
if (!CLAIM_COMMENT_MARKER.test(String(claim.row?.body ?? ''))) continue;
if (!markerMatches(CLAIM_COMMENT_MARKER, String(claim.row?.body ?? ''))) continue;
// Fail closed, twice: an unattributable claim cannot be matched against an
// author, and an unattributable candidate cannot be the seat that wrote it.
if (claim.author === null) continue;
Expand Down Expand Up @@ -1791,12 +1793,82 @@ export function claimRetractions(commentRows) {
*/
export const CLAIM_SELECTION_RULE =
'the GOVERNING claim — the NEWEST comment whose body carries a line beginning `Claim:`/`Claimed:` '
+ '(read through the sibling reader\'s ONE reading, `markerMatches`: the bare marker first, then the '
+ 'shared stripper per line, with a markdown LIST ITEM refused — so a decorated `**Claim:**` is the '
+ 'same record as a bare one and ⛔ the two readers of this thread cannot answer differently) '
+ 'AND whose `Branch:` line parses at least one protocol-shaped branch (newest by `created_at`; an '
+ 'unreadable stamp or a tie falls back to thread order, later row wins). The pool is every claim '
+ 'comment sharing that `created_at`; when NO claim names a branch at all, every claim comment is '
+ 'the pool. ⛔ Not earliest, ⛔ not a session match, ⛔ not the one whose body mentions the key. '
+ `MEMBERSHIP comes first: ${CLAIM_RETRACTION_RULE}`;

// ---------------------------------------------------------------------------
// #18764 — a DECORATED claim ENTERS the pool. ONE reading, and it is the
// sibling's.
//
// The pool and the retraction indexer each tested `CLAIM_COMMENT_MARKER`
// against the RAW body. The constant anchors the bare word at line start and
// tolerates leading whitespace and one `>` — nothing else — so a claim a seat
// wrote as `**Claim:** …` or `` `Claim:` … `` was not SUPERSEDED here, it was
// never a candidate: not listed, not rejected, not named anywhere in the
// record. Meanwhile `claimGovernance`, imported from the same sibling, had
// already been reading both markers through `markerMatches` since #18680, so
// ONE FUNCTION held both answers at once — governance saw the bolded claim and
// the pool beside it did not.
//
// Measured on the two live records the #18764 escalation named (read
// 2026-09-17T22:02Z; ⚠️ both cards have since left that state and the reading
// is stated with its time for that reason), replayed through `--pair-json`:
//
// a bolded/backticked claim carrying its own `Branch:` and `Clause-②: no`
// → `claim.selected: none — no comment on this thread carries a line
// beginning `Claim:``, the limb read MISPLACED and `--pair` exited 4,
// prescribing a `Clause-②-correction:` for a line the seat had already
// written in the right place.
// a BARE older claim declaring `yes` beneath a DECORATED newer one declaring
// `no` → the OLD claim governed the declaration, the reading answered
// `DECLARED yes`, and the newer record was not even listed as rejected.
// ⭐ That is the expensive direction: not a missing reading but a WRONG
// value, reported with every appearance of having been read.
//
// ⛔ The repair is NOT a `\*\*` added to `CLAIM_COMMENT_MARKER`. Decoration is
// an OPEN set (#18680 settled that), so admitting one spelling buys exactly
// that spelling and replays this card on the next one — and the constant is
// the PROTOCOL's spelling, which is why every reader imports it rather than
// restating it. ⛔ Nor is it a second undecorator written here: this file
// would then own a definition of "decorated" that the sibling could drift
// from, which is the very failure the card names. What both raw tests do
// instead is READ THROUGH `markerMatches` — the sibling's one reading, bare
// test first (so the change is provably additive: no body that matched
// yesterday stops matching) and the shared stripper after it, with the list
// item refused there and the near-miss vocabulary
// (`OWNERSHIP_MARKER_NEAR_MISS_FORMS`) kept where it lives. ⛔ That vocabulary
// is the sibling's and is not re-declared here.
//
// ## The OTHER undecorator in this file stays where it is, and that is MEASURED
//
// `undecorateRetractionLine` looks like the same job one section up, and the
// card asked whether the file should end up with one undecoration path. It
// should not, and the two paths were run over one fixture set to find out:
// 5 of 12 fixtures read DIFFERENTLY. `undecorateRetractionLine` strips `_` and
// then every leading non-letter/non-digit character, so it reads
// `__Claim:__`, `- Claim:`, `* Claim:`, `## Claim:` and `🚨 Claim:` as the
// directive; `markerMatches` reads none of those, deliberately — the list item
// and the heading are NAMED refusals with pins behind them, and the underscore
// form is a named near miss.
//
// ⭐ The divergence is load-bearing in BOTH directions, which is why unifying
// by hand would be a regression whichever way it went. The #18373 retraction
// this file's own battery replays opens `🚨 **撤回上一条认领…`, and the leading
// sigil is exactly what the prose channel must strip before the anchor can
// OPEN the line — narrowing the retraction stripper to the shared one would
// stop reading the measured specimen #18719 landed for. Widening the shared
// one the other way would make `- Claim:` a claim, which H20 pins as not one.
// ⇒ two strippers, two jobs, and the difference is stated here rather than
// discovered again. ⛔ Not unified from here: it is filed, with the fixture
// table, for the seat to route.
// ---------------------------------------------------------------------------

/**
* WHICH claim comment this reading is built from, and which it is not — the
* input half of the declaration limb (#18456).
Expand Down Expand Up @@ -1837,7 +1909,7 @@ export function claimCarrierSelection(commentRows) {
// ⭐ The full claim listing KEEPS the retracted rows: the record names them
// RETRACTED below. A pool that silently shrank would replace one invisible
// fact with another.
const claims = commentRows.filter((row) => CLAIM_COMMENT_MARKER.test(String(row?.body ?? '')));
const claims = commentRows.filter((row) => markerMatches(CLAIM_COMMENT_MARKER, String(row?.body ?? '')));
const live = claims.filter((row) => !retracted.has(row));
const governing = governance.governing;
const matched = governing ? live.filter((row) => (row?.created_at ?? null) === governing.createdAt) : [];
Expand Down Expand Up @@ -1898,13 +1970,17 @@ const CLAUSE2_CORRECTION_KEY_TEXT = 'Clause-②-correction';
* because the older claim happened to agree.
*
* The predicate that separates them is `CLAIM_COMMENT_MARKER`, imported rather
* than restated: a claim comment is one whose body carries a LINE BEGINNING
* `Claim:` (or `Claimed:`, optionally blockquoted), and that one spelling is
* the whole set — so a heading-style claim (`## Claim — …`) is not a claim
* comment here, however complete the reasoning under it, and its thread reads
* `absent`. ⛔ Widening the predicate is not this file's to do: it is the
* sibling's constant precisely so the two readers cannot drift, and the remedy
* for a thread that reads `absent` is a comment in the fixed spelling.
* than restated, and OFFERED through `markerMatches` — the sibling's one
* reading (#18764), imported for the same reason the constant is. A claim
* comment is one whose body carries a LINE BEGINNING `Claim:` (or `Claimed:`,
* optionally blockquoted), read bare first and then with the shared
* decoration stripped, so `**Claim:**` and `` `Claim:` `` are that same line
* and ⛔ not a second spelling this file admits on its own. A heading-style
* claim (`## Claim — …`) is still not a claim comment here, however complete
* the reasoning under it, and its thread reads `absent`. ⛔ Widening the
* predicate is not this file's to do: the constant AND the reading are the
* sibling's precisely so the two readers cannot drift, and the remedy for a
* thread that reads `absent` is a comment in the fixed spelling.
*
* @param {{ body?: string, created_at?: string }[]|null} commentRows — the REST
* comment rows, or `null` when the thread could NOT be read.
Expand Down Expand Up @@ -8108,6 +8184,82 @@ export async function selfTest() {
})());
t('⛔ CONTROL: the diagnosis KEY is unchanged, so every sentence already keyed to `comments` still finds it', readDiagnosisKey('comments', 770017) === 'comments:770017');

// -- #18764: a DECORATED claim ENTERS the pool -----------------------------
//
// The two live records the escalation named, replayed OFFLINE and ⛔ never
// re-graded: they are another repository's cards and another seat's work.
// Ids, stamps and the load-bearing LINES are the real ones, read from the
// REST rows at 2026-09-17T22:02Z — ⚠️ both cards have since left the state
// they were read in, which is why the reading carries its time. The bodies
// below ADD the `Branch:` / `Clause-②` lines the real comments did not carry:
// without them the thread reads `claim-branch-unparsed` on BOTH sides of this
// change (measured), and the defect this battery pins is the one that only
// shows once a claim is otherwise complete.
battery('#18764: a DECORATED claim ENTERS the pool — ONE reading, and it is the sibling\'s');
const D64 = (id, at, body, login = 'os-sales') => ({ id, created_at: at, user: { login }, body });
const D64_BOLD = D64(5721120402, '2026-09-17T20:57:09Z', [
'**Claim:** card objectui#9660, by the `domain:spec` @ objectui execution seat, session `session_01UanLVj6xvbS6puBCewLr8L`.',
'Branch: `claude/issue-9660-named-test-invocation`',
'Clause-②: no',
].join('\n'));
const D64_TICK = D64(5720184809, '2026-09-17T19:41:36Z', [
'`Claim:` card objectui#9717, by the `domain:spec` @ objectui execution seat, session `session_01UanLVj6xvbS6puBCewLr8L`.',
'Branch: `claude/issue-9717-doc-component-types`',
'Clause-②: no',
].join('\n'));
// The older BARE claim, declaring the OPPOSITE value — so "the wrong carrier
// governs" is a WRONG VALUE here and not merely a missing one.
const D64_BARE_OLD = D64(5700000001, '2026-09-17T18:00:00Z', [
'Claim: the older BARE claim',
'Branch: `claude/issue-9660-older-bare`',
'Clause-②: yes',
].join('\n'));
const D64_POOL = (rows) => claimCarrierSelection(rows).pool.map((r) => r.id).join(',');
const D64_RECORD = (rows) => {
const rec = pairInputRecord({ pr: 9999, card: 9660, cardComments: rows, headSha: 'offline' });
return { selected: [rec['claim.selected']].flat().join('\n'), rejected: [rec['claim.rejected']].flat().join('\n') };
};

t('⭐ a BOLD claim ENTERS the pool — the state it could not reach at all before', D64_POOL([D64_BOLD]) === String(D64_BOLD.id), D64_POOL([D64_BOLD]));
t('…and GOVERNS: the branch resolved is the one IT names', (claimCarrierSelection([D64_BOLD]).governing?.branches ?? []).join() === 'claude/issue-9660-named-test-invocation');
t('…and the declaration limb reads the `Clause-②` line OFF IT', cardDeclaration([D64_BOLD]).state === 'declared' && cardDeclaration([D64_BOLD]).value === 'no');
t('⛔ CONTROL — the CONSTANT is NOT widened: the raw marker still refuses that same body', CLAIM_COMMENT_MARKER.test(D64_BOLD.body) === false);
t('⭐ the BACKTICKED spelling is the same record, by the same reading', D64_POOL([D64_TICK]) === String(D64_TICK.id));
t('…with its own `Branch:` line resolved', (claimCarrierSelection([D64_TICK]).governing?.branches ?? []).join() === 'claude/issue-9717-doc-component-types');
t('…and its own declaration read', cardDeclaration([D64_TICK]).value === 'no');
t('⛔ CONTROL: the raw marker refuses the backticked body too', CLAIM_COMMENT_MARKER.test(D64_TICK.body) === false);
t('⭐ a DECORATED NEWER claim SUPERSEDES a BARE older one — the pool is the newest, not the readable one', D64_POOL([D64_BARE_OLD, D64_BOLD]) === String(D64_BOLD.id), D64_POOL([D64_BARE_OLD, D64_BOLD]));
t('…and the older record is LISTED, as SUPERSEDED rather than dropped', says(D64_RECORD([D64_BARE_OLD, D64_BOLD]).rejected, 'a SUPERSEDED claim') && says(D64_RECORD([D64_BARE_OLD, D64_BOLD]).rejected, String(D64_BARE_OLD.id)));
t('⭐ …and the VALUE the limb reads is the newer one', cardDeclaration([D64_BARE_OLD, D64_BOLD]).value === 'no');
t('⛔ CONTROL: the older record declares the OPPOSITE, so selecting the wrong carrier is a WRONG value, ⛔ not a missing one', cardDeclaration([D64_BARE_OLD]).value === 'yes');
t('the input record NAMES the decorated row it selected — by id and by date', says(D64_RECORD([D64_BOLD]).selected, String(D64_BOLD.id)) && says(D64_RECORD([D64_BOLD]).selected, '2026-09-17T20:57:09Z'));
t('⛔ CONTROL: the same thread read the other way says nobody claimed at all', says(D64_RECORD([D64(1, '2026-09-17T20:57:09Z', 'no claim on this line')]).selected, 'no comment on this thread carries'));

// The retraction index reads the SAME predicate, so a decorated claim is
// retractable by its own author — ⛔ never a record that can be written but
// never withdrawn.
const D64_RELEASE = D64(5721120999, '2026-09-17T21:30:00Z', 'Release: session `session_01UanLVj6xvbS6puBCewLr8L` — 去向 `pm:queue`');
t('⭐ the retraction index SEES a decorated claim — it is retractable by its own author', claimRetractions([D64_BOLD, D64_RELEASE]).has(D64_BOLD));
t('…and the pool then says every claim on the thread is RETRACTED, ⛔ not that none was written', says(D64_RECORD([D64_BOLD, D64_RELEASE]).selected, 'RETRACTED'));
t('⛔ CONTROL: a DIFFERENT author\'s release retracts nothing, decorated or not', claimRetractions([D64_BOLD, { ...D64_RELEASE, user: { login: 'os-other' } }]).size === 0);

// The refusals are the SIBLING's and are pinned here as still-refused: this
// file admits no spelling of its own, so a form the sibling names as a NEAR
// MISS must not become a claim by arriving through this door.
t('⛔ a markdown LIST ITEM is still not a claim — the shape the shared reading refuses to undecorate through', D64_POOL([D64(2, '2026-09-17T20:00:00Z', '- Claim: seat.\nBranch: `claude/issue-1-x`')]) === '');
t('⛔ a HEADING-style claim is still not one', D64_POOL([D64(3, '2026-09-17T20:00:00Z', '## Claim: seat.\nBranch: `claude/issue-1-x`')]) === '');
t('⛔ UNDERSCORE emphasis is still a NAMED near miss, ⛔ not a claim', markerMatches(CLAIM_COMMENT_MARKER, '__Claim:__ seat.') === false);
t('⛔ and the `Clause-②-correction:` comment does not enter the pool through the new door either', D64_POOL([FIXED_CORRECTION('no')]) === '' && markerMatches(CLAIM_COMMENT_MARKER, FIXED_CORRECTION('no').body) === false);
t('⛔ provably ADDITIVE: a bare claim this file already read reads exactly as before', claimCarrierSelection([CLAIM('Clause-②: no')]).pool.length === 1);

// ⭐ The file keeps TWO undecoration paths, and that is a measurement rather
// than an oversight: `undecorateRetractionLine` strips `_` and then every
// leading non-letter/non-digit character, which the shared stripper does not.
// 5 of 12 fixtures read differently. Both directions are load-bearing, so the
// pair below pins the divergence instead of quietly closing it.
t('⭐ the RETRACTION path still reads the sigil-led #18373 line — narrowing it to the shared reading would unland #18719', claimRetractions(RTX_18373).has(RTX_18373.find((r) => r.id === RTX_WITHDRAWN)));
t('⭐ …while the SHARED reading does not strip a leading sigil, so the two paths are ⛔ NOT interchangeable', markerMatches(CLAIM_COMMENT_MARKER, '🚨 Claim: PM loop round 1') === false);

// -- The floor: every declared battery RAN, and ran its cases (#13489) -----
//
// Evaluated after every battery has had its chance and BEFORE the verdict, so
Expand Down Expand Up @@ -8176,7 +8328,9 @@ export async function selfTest() {
+ 'line that QUOTES the spelling held apart from one that declares a value in BOTH halves '
+ 'of that property, the input record whose field roster is the same on exit 0, on exit 4 '
+ 'and on a refusal — with the selected carrier, its body fingerprint and the rejected '
+ 'candidates each stated — and the exit register).',
+ 'candidates each stated, the DECORATED claim that enters the pool and governs through '
+ 'the sibling\'s one reading — the constant unwidened, and the file\'s two undecoration '
+ 'paths pinned as the two jobs they are — and the exit register).',
);

selfTestReachedVerdict = true;
Expand Down
Loading