From af8877ed608e5544c66ae84f3abd1a57088659c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 18:56:40 +0000 Subject: [PATCH 1/3] fix(pm): H44's seat leg reaches the triage post whatever its holder (#18324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `h44NeedsSeatComments` was `pm:seat` + `seatIsHeld`, and `seatIsHeld` excludes a `🟢 Routine` holder by name and every non-🟢 status. The triage seat runs as a Routine or sits vacant most of the time — it read `🔴 空缺` on the live board at the filing — so H44, H56 and H64 read NOTHING on the one post H44's own header names as its worked example, while H65 read it every run through `h65IsTriageSeatPost`. The gate now admits the triage post through that same predicate, beside the held leg rather than instead of it: the file keeps ONE reading of 「is this the triage post」. Measured cost: ZERO additional requests, because H65 already buys exactly that page every run through the shared `seatPostRowsFor` memo, so the widening only moves which row asks first. ⛔ Not a vacant-seat posture, and that is measured rather than assumed: all three rows accuse a COMMENT and name its author, never the post's holder. On the vacant triage post's newest page (18 comments, 2026-09-10 → 2026-09-15) H44 files one carrier row naming 10 offending comments, H56 files 0, H64 files 0. H32's population is untouched — its subject is an accusation that a named holder is not working, which is what the held gate is for. Self-test 4235 → 4245 cases. The H65 case that pinned the old behaviour is rewritten in place, not deleted, with the superseded statement quoted above it. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr --- scripts/pm/check-half-states.mjs | 106 ++++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 16 deletions(-) diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index b42680717df..d433b815109 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -6986,15 +6986,42 @@ export function h32NeedsSeatComments(issue) { * measured on the live board while this row was written, H32's gate fetched 4 of * 13 open seat posts, and the 9 it skipped included the TRIAGE seat — carrier of * the round-open marker that is this card's worked example — and the SKILLS - * seat. So this gate keeps the `pm:seat` + HELD legs and drops the lane test. - * - * The cost is stated where it is paid: +6 comment pages per run at that board. - * ⛔ H32's population is untouched, and H38 — which inherited the same gate for - * the same reason — takes this widening under its own card, not here. + * seat. So this gate drops the lane test. + * + * ## The HELD leg had the same defect one step further in (#18324) + * + * ⚠️ The paragraph above names the TRIAGE post as this row's worked example, and + * the held leg is exactly what kept the row off it: `seatIsHeld` excludes a + * `🟢 Routine` holder BY NAME and every non-🟢 status, while the triage seat runs + * as a Routine or sits vacant most of the time. Measured on the live board at + * the filing, the post read `[PM seat] triage (objectstack-wide) — 🔴 空缺 …`, so + * H44, H56 and H64 read NOTHING on it while H65 — whose own population carries + * no held gate — read it every run. A report-only row that cannot see its own + * worked example is dead letter, so the triage post is admitted BESIDE the held + * leg, never instead of it, through `h65IsTriageSeatPost` — the file's ONE + * reading of 「is this the triage post」, reused rather than re-spelled. + * + * ⛔ It is NOT a vacant-seat posture, and that is MEASURED rather than assumed. + * All three rows accuse a COMMENT and name its author; none of them names the + * post's current holder, so 「nobody is on the clock」 changes nothing about + * whether an artefact on the thread states a reading with no time on it. On the + * vacant triage post's newest page (18 comments, 2026-09-10T23:26:55Z → + * 2026-09-15T15:54:40Z, read 2026-09-15): H44 files ONE carrier row naming 10 + * offending comments, H56 files 0 and H64 files 0. + * + * The cost is stated where it is paid. The lane widening (#14929) was +6 comment + * pages per run at that board; this one is ZERO — H65 already buys exactly that + * page every run through the same `seatPostRowsFor` memo, so widening this gate + * moves WHO asks first and changes no total. + * + * ⛔ H32's population is untouched by either widening: its subject is an + * accusation that a NAMED holder is not working, which is what the held gate is + * for and what this row's subject is not. H38 reads H32's population and keeps + * it — what #18325 moved there is the WINDOW, not the population. */ export function h44NeedsSeatComments(issue) { if (!labelNames(issue ?? {}).includes('pm:seat')) return false; - return seatIsHeld(issue); + return seatIsHeld(issue) || h65IsTriageSeatPost(issue); } /** @@ -9581,8 +9608,19 @@ export function h43GovernedReviewRequestGap(pr, governed, approvers, reviewed = // every HELD seat post regardless of lane shape: +6 requests per run at that // board, 4 runs a day. ⛔ H32's own population and lane gate are untouched — its // exclusion is argued from a lane INVENTORY it cannot count, which is a real -// argument for H32 and not this row's to overturn. H38 takes the same widening -// under its own card. +// argument for H32 and not this row's to overturn. +// +// ⚠️ …and the TRIAGE post whether or not it is HELD (#18324). The widening above +// stopped one leg short: `seatIsHeld` excludes a `🟢 Routine` holder BY NAME and +// every non-🟢 status, so the post this banner names as the worked example was +// still unread whenever the triage seat ran as a Routine or sat vacant — which +// it does most of the time, and did at that filing (`🔴 空缺`). It is admitted +// through `h65IsTriageSeatPost`, the file's one reading of that question, and it +// costs NOTHING: H65 buys exactly that page every run through the same memo, so +// this leg only changes which row asks first. ⛔ Not a vacant-seat posture — +// these three rows accuse a COMMENT and name its author, never the post's +// holder. H38 reads H32's population and keeps it; what its own card moved +// (#18325) is the WINDOW those rows read, not the population. // // ## The seat-post WINDOW is the thread's NEWEST page, and that is measured // @@ -20116,7 +20154,8 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // request returns 2026-08-06 → 2026-08-10, five weeks of archive holding none // of the round then running. Every row that reads a seat post's comments — // H44, H56 and H64 through the cache, H65 through this memo — was reporting on - // that archive. + // that archive. (#18324 widened the first of those to the triage post + // whatever its holder.) // // So the page is located from the carrier's own `comments` count, through // H65's two exported helpers rather than a second copy of the arithmetic, and @@ -21760,12 +21799,17 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // page」. That is no longer the state of the file: a seat post's cached thread // IS its newest page now, filled by H44's seat leg from the same // `seatPostRowsFor` memo this row reads, so the two are not looking at - // different windows any more. What keeps the row here is the other half of - // its population: `h65IsTriageSeatPost` admits a `🟢 Routine` or vacant triage - // seat that `h44NeedsSeatComments` excludes by name, so this row must still be - // able to BUY, and a purchase belongs where every free read is already done. - // Moved above H44 it would also start deciding which posts land in that row's - // corpus, which is the widening those headers forbid. + // different windows any more. The second half of that argument is spent too + // (#18324): `h44NeedsSeatComments` now admits the triage post whether or not + // it is held, through this row's own `h65IsTriageSeatPost`, so H44's seat leg + // reaches every post in this population and the page is always already + // memoised by the time this runs. ⛔ That is not a reason to move the row UP: + // a purchase belongs where every free read is already done, the memo is what + // makes this row cost nothing WHEREVER it sits, and moved above H44 it would + // start deciding which posts land in that row's corpus — the widening those + // headers forbid. What it does mean is that the request this row still knows + // how to buy is now a FALLBACK rather than the normal path: a triage post the + // seat leg failed to read is bought here, and one it read is free. const h65Posts = new Map(); for (const [number, issue] of seenUnscoped) { if (h65IsTriageSeatPost(issue)) h65Posts.set(number, issue); @@ -28893,6 +28937,28 @@ Mutual exclusion: \`get_comments\` page 747 → \`[]\`, page 746 = my own R+117 t('H44 gate: a VACANT seat is fetched by neither', h44NeedsSeatComments(seat44('[PM seat] domain:devx — ⏳ vacant')), false); t('H44 gate: a non-seat card is out of scope', h44NeedsSeatComments(seat44('[PM seat] domain:spec — 🟢 os-zhuang', ['pm:queue'])), false); + // ⑦b The TRIAGE leg (#18324) — the post this row's own header names as its + // worked example, admitted whatever its holder. ⭐ The four cases below are + // the statement that was FALSE before this leg existed: on the live board the + // triage seat reads `🔴 空缺` or `🟢 Routine` most of the time, and the held + // gate alone excluded both by name. + const TRIAGE_VACANT = seat44('[PM seat] triage (objectstack-wide) — 🔴 空缺 · 上一任 session_x 留简报'); + const TRIAGE_ROUTINE = seat44('[PM seat] triage (objectstack-wide) — 🟢 Routine'); + t('H44 gate: ⭐ a VACANT triage post IS fetched — the held leg alone made this row dead letter on it', h44NeedsSeatComments(TRIAGE_VACANT), true); + t('H44 gate: ⭐ …and a `🟢 Routine` triage post too, which `seatIsHeld` excludes BY NAME', h44NeedsSeatComments(TRIAGE_ROUTINE), true); + t('H44 gate: the held leg is what is doing NONE of that work — both are unheld', seatIsHeld(TRIAGE_VACANT) || seatIsHeld(TRIAGE_ROUTINE), false); + t('H44 gate: …so the triage leg is the whole of the difference, and it is H65\'s own reading', h65IsTriageSeatPost(TRIAGE_VACANT) && h65IsTriageSeatPost(TRIAGE_ROUTINE), true); + // ⛔ The widening is the TRIAGE post and nothing else: an unheld EXECUTION + // seat stays out, and H32's population does not move at all. + t('H44 gate: ⛔ an unheld `domain:*` seat is still out — the leg is the triage post, not every post', h44NeedsSeatComments(seat44('[PM seat] domain:spec — 🔴 vacant')), false); + t('H44 gate: ⛔ …nor does an unheld `repo:*` seat come in', h44NeedsSeatComments(seat44('[PM seat] repo:cloud — ⏳ vacant')), false); + t('H44 gate: ⛔ H32 does not follow it onto the vacant triage post', h32NeedsSeatComments(TRIAGE_VACANT), false); + t('H44 gate: ⛔ …nor onto the Routine one — that gate is still `pm:seat` + HELD + countable lane', h32NeedsSeatComments(TRIAGE_ROUTINE), false); + // What the three rows then REPORT on a vacant post is a reading about the + // COMMENT, never about the holder — which is why no vacant-seat posture is + // owed. The body below is the shape measured on the live post's newest page. + t('H44 gate: …and the row it then files reads the COMMENT, so a vacant holder changes nothing', h44hit('分诊轮收尾 — 本轮 7 open cards 已派完。', true).shape, 'a seat-post section'); + // ⑧ Grammar hygiene. A `g` flag would carry `lastIndex` between calls and make // this row's answer depend on how many comments preceded it. t('H44 grammar: ⛔ no fragment regex is sticky', H44_READING_FRAGMENTS.every((f) => !f.re.global), true); @@ -31240,7 +31306,15 @@ Doubles as the fire's **write self-check** (step 0). \`201\` is not the reading. t('H65 scope: a missing card does not crash', h65IsTriageSeatPost(undefined), false); t('H65 scope: ⭐ a `🟢 Routine` triage seat IS in population — the seat runs as a Routine', h65IsTriageSeatPost(seat65('[PM seat] triage (objectstack-wide) — 🟢 Routine')), true); t('H65 scope: …and that is exactly what `seatIsHeld` would have excluded by name', seatIsHeld(seat65('[PM seat] triage (objectstack-wide) — 🟢 Routine')), false); - t('H65 scope: …so inheriting H44\'s seat gate would have silenced this row', h44NeedsSeatComments(seat65('[PM seat] triage (objectstack-wide) — 🟢 Routine')), false); + // ⚠️ REWRITTEN (#18324), not deleted: this case used to read 「…so inheriting + // H44's seat gate would have silenced this row」 and pinned that gate at + // `false` on a `🟢 Routine` triage post. That is no longer what the file does + // — H44's seat leg now admits the triage post through THIS row's predicate — + // so the case pins the fact that replaced it. The reason this row does not + // inherit that gate is unchanged and is the sentence above: `seatIsHeld` is + // about accusing a named holder, and this row's subject is an artefact. + t('H65 scope: ⭐ H44\'s seat leg now REACHES the Routine triage post, through this very predicate', h44NeedsSeatComments(seat65('[PM seat] triage (objectstack-wide) — 🟢 Routine')), true); + t('H65 scope: …and it is the triage leg carrying it, since the held leg still reads false', seatIsHeld(seat65('[PM seat] triage (objectstack-wide) — 🟢 Routine')), false); // ⭐ H44 is UNTOUCHED, and the measured reason this row could not lean on it. t('H65 adjacency: ⛔ H44\'s round-open regex does NOT match the current marker spelling', H44_ROUND_OPEN_MARKER.test(MARKER65), false); From 4c022746ce676378ee4319c775213ce70d3e7d8a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 18:59:24 +0000 Subject: [PATCH 2/3] fix(pm): H32 and H38 take their seat marker from the post's NEWEST page (#18325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `seatMarkers` gather captured `commentRowsFor`'s page-less FIRST page, and GitHub serves issue comments OLDEST-FIRST, so `latestSeatMarker` returned the newest row of page 1. Measured on the live board: #6023's 225-comment thread yielded 2026-08-27T16:38:35Z while its real latest marker was 2026-09-14T11:41:59Z, 18 days newer; #6367 yielded 2026-09-04 against a real 2026-09-10. That row is H32's idle clock AND carries H32's wait exemption, and it is H38's `T_seat` — so the one reading on a seat post that must be the newest was the oldest page the endpoint serves. The gather now takes the window through `seatPostRowsFor`, the file's one declaration of a seat post's window. It buys nothing new: this population is a SUBSET of H44's seat leg, the memo is shared, and asking here only makes the gather loop the first asker. The page-less fetch it replaces was itself wasted — H44's leg overwrote that cache entry a pass below. `latestSeatMarker`'s contract is unchanged; a null page is left out of `seatMarkers` exactly as the throw was. ⚠️ H38's live verdicts rarely move, and the self-test pins why rather than leaving it to be discovered: `seatPostLastEventMs` maxes the rows against the post's own `updated_at`, which GitHub bumps on every new comment, so the page choice decides `T_seat` exactly where that field cannot — a comment EDITED after the post's last bump. `seatWindowAudit`'s reader pin is rewritten 2 -> 3 with the superseded statement quoted above it; its page-less (1), commentPaths (4) and purchase-site (1) readings are unchanged. Self-test 4245 -> 4261 cases. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr --- scripts/pm/check-half-states.mjs | 141 +++++++++++++++++++++++++++---- 1 file changed, 126 insertions(+), 15 deletions(-) diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index d433b815109..5032b93954b 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -9641,17 +9641,25 @@ export function h43GovernedReviewRequestGap(pr, governed, approvers, reviewed = // declaration of 「which page is a seat post's window」. It is ONE request per // seat post per run and it is SHARED: the leg writes it into `commentCache` // before the judging loop, so H56 and H64 — which read this row's corpus by -// construction — get the same page, and H65 consumes the same memo at the foot -// instead of buying a second copy of it. +// construction — get the same page; H65 consumes the same memo at the foot +// instead of buying a second copy of it; and H32 and H38 take it at the top of +// the sweep (#18325), which makes the gather loop the first asker on every post +// in both populations rather than a second buyer. // // Three prices, each stated where it is paid: // // • A seat post whose thread fits in ONE page costs nothing new: page 1 IS // the newest page, and the rows already in the cache are reused unfetched. -// • A HELD multi-page seat post that H32 already fetched costs +1 request — -// H32 bought page 1 for its own corpus, which this row ⛔ does not touch -// (H32/H38 keep reading exactly the rows they read before, captured in -// `seatMarkers` before this leg runs). +// • A HELD multi-page seat post in H32's population costs this leg NOTHING — +// ⚠️ REWRITTEN (#18325). It used to read 「costs +1 request — H32 bought +// page 1 for its own corpus, which this row ⛔ does not touch (H32/H38 keep +// reading exactly the rows they read before)」, and that was true only while +// H32 bought a page-less FIRST page nobody wanted: its clock and H38's +// `T_seat` were reading an archive, and this leg then overwrote that cache +// entry a pass below. H32 now takes the same window through the same memo, +// so the page is bought ONCE, at the gather loop, and this leg finds it +// memoised. H32's POPULATION is still untouched — what moved is which page +// it reads, never which posts it reads. // • A seat post with no readable `comments` count cannot have its newest page // LOCATED, so the window falls back to page 1 — today's reading, never a // better one — and the summary clause counts those separately rather than @@ -20155,7 +20163,8 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // of the round then running. Every row that reads a seat post's comments — // H44, H56 and H64 through the cache, H65 through this memo — was reporting on // that archive. (#18324 widened the first of those to the triage post - // whatever its holder.) + // whatever its holder; #18325 moved H32 and H38 onto this memo too, so the + // gather loop is the first asker and every later reader is free.) // // So the page is located from the carrier's own `comments` count, through // H65's two exported helpers rather than a second copy of the arithmetic, and @@ -20422,11 +20431,36 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // The ROWS are kept, not just the derived marker: H38 needs every // comment stamp on the post to compute `T_seat`, and re-fetching for // it would double a cost this gate exists to bound. H32 reads the - // same rows through `latestSeatMarker` exactly as before. - const rows = await commentRowsFor(issue); - const marker = latestSeatMarker(rows); - stats.seatMarkersRead = (stats.seatMarkersRead ?? 0) + 1; - seatMarkers.set(issue.number, { issue, marker, rows }); + // same rows through `latestSeatMarker`, whose contract is unchanged. + // + // ⚠️ The WINDOW is `seatPostRowsFor` — the file's one declaration of + // 「which page of a seat post's thread this sweep reads」 — and ⛔ not + // a page-less `commentRowsFor` (#18325). GitHub serves issue comments + // OLDEST-FIRST, so the page-less request this line used to make + // returned the thread's FIRST page and `latestSeatMarker` returned + // the newest row OF THAT PAGE: measured on the live board, #6023's + // 225-comment thread yielded 2026-08-27T16:38:35Z while its real + // latest marker was 2026-09-14T11:41:59Z, 18 days newer. That row is + // H32's IDLE CLOCK and carries H32's wait exemption, and it is H38's + // `T_seat` — so the one reading on this post that must be the newest + // was the oldest page the endpoint serves. + // + // It buys NOTHING that was not already bought: this population is a + // SUBSET of H44's seat leg (`pm:seat` + held + countable lane, inside + // `pm:seat` + held), the memo is shared, and asking here simply makes + // this loop the first asker. The page-less fetch it replaces was + // itself wasted — H44's leg overwrote that `commentCache` entry with + // the newest page a pass below. + const page = await seatPostRowsFor(issue); + // `seatPostRowsFor` returns `null` for a page it could not read, and + // memoises that null. Treated exactly as the throw below is: the seat + // is left out rather than judged on nothing (#4690). + if (page) { + const rows = page.rows; + const marker = latestSeatMarker(rows); + stats.seatMarkersRead = (stats.seatMarkersRead ?? 0) + 1; + seatMarkers.set(issue.number, { issue, marker, rows }); + } } catch { // Left out of `seatMarkers` entirely: the predicate's `undefined` // and `null` both decline, and the coverage pair is what states the @@ -21175,6 +21209,19 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // Its seat population is exactly `h32NeedsSeatComments`' — HELD seats on a // lane this board can count — because that gate is what decided which seat // threads were READ, and a row must not speak about a post nobody fetched. + // + // ⚠️ Its WINDOW moved with H32's (#18325) and the placement argument above is + // unchanged by it: `rows` is still whatever the gather loop captured, still + // read here and nowhere else, and still costs this row no request. What those + // rows now are is the post's NEWEST page rather than its oldest, so `T_seat` + // is computed over the comments of the round that is actually running. ⚠️ On + // the live board that rarely MOVES a verdict, and the reason is worth stating + // rather than discovering: `seatPostLastEventMs` maxes the comment stamps + // against the post's own `updated_at`, and GitHub bumps `updated_at` when a + // comment is added, so the post's own field already dominates. The page + // choice decides `T_seat` exactly where it cannot — a comment EDITED after + // the post's last bump, which is the one 「seat writing home」 signal that + // lives only in the rows. // ⚠️ The residual, named rather than left implicit: a VACANT seat post whose // lane is still taking claims is not reported here. That shape is real (it is // dispatch on a lane whose seat says nobody is on the clock) but it is a @@ -21209,8 +21256,12 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // three the thread's oldest 100 comments: on the triage post, five weeks of // archive with none of the current round on it. // - // ⛔ H32's rows are not moved by this. It captured its own `seatMarkers` rows - // before this line runs, and H38 read them one pass above. + // ⛔ H32's rows are not moved by this, and since #18325 they no longer need to + // be: the gather loop captured `seatMarkers` from this same memo, at this same + // window, before this line runs, and H38 read them one pass above. So the + // cache write below can only REPEAT what those rows already hold for a post in + // both populations, and the posts this leg adds (an unheld triage post, a + // `repo:*` seat) are ones H32 never speaks about. const h44Seats = [...seen.values()].filter((issue) => h44NeedsSeatComments(issue)); stats.readingSeatCandidates = h44Seats.length; for (const issue of h44Seats) { @@ -29026,7 +29077,11 @@ Mutual exclusion: \`get_comments\` page 747 → \`[]\`, page 746 = my own R+117 // counted by the file-wide audit it exists to check (`RETIRED_ASSIGNEE_COINAGE`'s // reason, one row over). t('seat window: the located page is REQUESTED in exactly one place', audit18312.purchaseSites, 1); - t('seat window: …and taken through the memo by more than one row, which is what makes it shared', audit18312.readers, 2); + // ⚠️ REWRITTEN (#18325), not deleted: this pin read `2` while the memo's + // readers were H44's seat leg and H65. H32's gather is the third, and it is + // the FIRST to ask on every post in both populations — which is what makes + // the count a statement about sharing rather than about cost. + t('seat window: …and taken through the memo by more than one row, which is what makes it shared', audit18312.readers, 3); const TICK18312 = String.fromCharCode(96); const commentsPath18312 = (query) => `${TICK18312}/repos/\${OWNER_REPO}/issues/\${issue.number}/comments?${query}${TICK18312}`; @@ -29035,6 +29090,62 @@ Mutual exclusion: \`get_comments\` page 747 → \`[]\`, page 746 = my own R+117 t('seat window: the audit SEES a second purchase site when one exists', seatWindowAudit(['h65CommentPagePath(', 'OWNER_REPO, a)\n', 'h65CommentPagePath(', 'OWNER_REPO, b)'].join('')).purchaseSites, 2); t('seat window: …and the assembled needles did not defeat the file-wide audit', seatWindowAudit().purchaseSites, 1); + // -- H32's clock and H38's `T_seat` read the NEWEST page (#18325) ---------- + // + // The gather that fills `seatMarkers` lives inside `sweepInto`, which takes no + // injectable transport, so the property is pinned where it is decidable: the + // two pages a multi-page seat post can serve, and what each of the three + // readings (`latestSeatMarker`, its age, `seatPostLastEventMs`) returns off + // each. ⭐ The fixture is the measured live shape of #6023 — 225 comments, so + // page 1 ends 2026-08-27 while the real latest marker is 2026-09-14. + const at18325 = (iso, body) => ({ created_at: iso, updated_at: iso, body }); + const PAGE1_18325 = [ + at18325('2026-08-08T05:34:17Z', 'R1 开场:队列 12 open cards。'), + at18325('2026-08-27T16:38:35Z', '本轮等 CI 收敛后再派。'), + ]; + const LASTPAGE_18325 = [ + at18325('2026-09-08T05:16:07Z', 'R1 wave 2 派发 4 张。'), + at18325('2026-09-14T11:41:59Z', '继承落地债 4/4 已清,PR 已入队。'), + ]; + const SEAT_18325 = { ...seat18312(225), updated_at: '2026-09-14T11:41:59Z' }; + const NOW_18325 = Date.parse('2026-09-15T19:00:00Z'); + // ⭐ BEFORE — the page-less first page, which is what this row used to read. + t('seat marker window: ⛔ page 1 makes the seat\'s LATEST utterance an August one', latestSeatMarker(PAGE1_18325).createdAt, '2026-08-27T16:38:35Z'); + t('seat marker window: …so H32\'s idle clock reads ~19 days, not hours', Math.round(seatMarkerAgeMinutes(latestSeatMarker(PAGE1_18325), NOW_18325) / 1440), 19); + t('seat marker window: …and the wait exemption is read off that August comment', seatDeclaresWait(latestSeatMarker(PAGE1_18325).body), true); + // ⭐ AFTER — the located newest page, which is what the memo hands it now. + t('seat marker window: ⭐ the newest page makes it the seat\'s real latest utterance', latestSeatMarker(LASTPAGE_18325).createdAt, '2026-09-14T11:41:59Z'); + t('seat marker window: …so the clock reads the round that is actually running', Math.round(seatMarkerAgeMinutes(latestSeatMarker(LASTPAGE_18325), NOW_18325) / 1440), 1); + t('seat marker window: …and the exemption is judged on the CURRENT marker, which declares no wait', seatDeclaresWait(latestSeatMarker(LASTPAGE_18325).body), false); + // ⛔ `latestSeatMarker`'s own contract is untouched: hand it either page and + // it still returns the newest row OF WHAT IT WAS GIVEN. The defect was never + // in this function; it was in which rows reached it. + t('seat marker window: ⛔ the resolver is unchanged — newest row of whatever page it is handed', latestSeatMarker([...PAGE1_18325, ...LASTPAGE_18325]).createdAt, '2026-09-14T11:41:59Z'); + t('seat marker window: …and thread order still breaks a tie no stamp can', latestSeatMarker([{ created_at: 'x', body: 'first' }, { created_at: 'x', body: 'second' }]).body, 'second'); + // ⭐ H38's `T_seat`. ⚠️ On the live board the page rarely moves it, because + // `seatPostLastEventMs` maxes the rows against the post's own `updated_at` + // and GitHub bumps that on every new comment — the first case pins exactly + // that, so the small live delta is a measured property and not a surprise. + t('H38 T_seat: the post\'s own `updated_at` dominates, so both pages agree here', seatPostLastEventMs(SEAT_18325, PAGE1_18325), seatPostLastEventMs(SEAT_18325, LASTPAGE_18325)); + // …and the one signal that lives ONLY in the rows: a comment EDITED after the + // post's last bump. That is the seat writing home, and page 1 cannot see it. + const EDITED_18325 = [ + { created_at: '2026-09-10T09:00:00Z', updated_at: '2026-09-15T08:00:00Z', body: 'R1 wave 3 记账(已编辑)' }, + ]; + t('H38 T_seat: ⭐ a comment edited after the post\'s last bump only reaches it through the newest page', seatPostLastEventMs(SEAT_18325, EDITED_18325), Date.parse('2026-09-15T08:00:00Z')); + t('H38 T_seat: ⛔ …and page 1 leaves `T_seat` back at the post\'s own bump, one day stale', seatPostLastEventMs(SEAT_18325, PAGE1_18325), Date.parse('2026-09-14T11:41:59Z')); + // ⭐ The consequence H38 actually files, both ways round: a lane claim written + // between the two readings is STALE on page 1's `T_seat` and CLEAN on the + // newest page's — the verdict flip this card is about. + const CLAIM_18325 = { number: 18324, at: Date.parse('2026-09-14T20:00:00Z') }; + const LANE_SEAT_18325 = { ...SEAT_18325, title: '[PM seat] domain:devx @ objectstack — 🟢 os-project-manager' }; + t('H38 verdict: ⛔ on the OLD window the post reads STALE against a claim it already answered', typeof h38SeatPostStale(LANE_SEAT_18325, Date.parse('2026-09-14T11:41:59Z'), CLAIM_18325), 'string'); + t('H38 verdict: ⭐ …and CLEAN once `T_seat` comes off the newest page', h38SeatPostStale(LANE_SEAT_18325, Date.parse('2026-09-15T08:00:00Z'), CLAIM_18325), null); + // ⭐ The population is what did NOT move. Both gates read exactly as before. + t('seat marker window: ⛔ H32\'s population is unmoved — a vacant seat still buys no fetch', h32NeedsSeatComments({ ...SEAT_18325, title: '[PM seat] domain:devx — ⏳ vacant' }), false); + t('seat marker window: ⛔ …and a foreign-lane seat still buys none', h32NeedsSeatComments({ ...SEAT_18325, title: '[PM seat] repo:cloud — 🟢 os-zhuang' }), false); + t('seat marker window: …while the held own-board seat it DOES read is a subset of H44\'s leg, which is why the memo costs nothing', h32NeedsSeatComments(SEAT_18325) && h44NeedsSeatComments(SEAT_18325), true); + // -- H44's marker spelling (#18312) ---------------------------------------- // // ⭐ The measured body: comment 5682407231 on the triage seat post, the From 432a90a2ede6ce9a247a51ddc9a586d4be88a1a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 19:17:04 +0000 Subject: [PATCH 3/3] fix(pm): H44's summary clause names the triage leg it now reads (#18324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live run on the widened leg printed `5 of 5 HELD seat post(s)` while one of those five was the VACANT triage post — the clause described its own count wrongly on every run. Measured against `origin/main`: the leg reads 4 of 4 there and 5 of 5 here, and the fifth is #6015. The clause now names both legs. Its numbers are unchanged, and the existing pin that asserted the HELD wording is rewritten in place with the superseded string quoted above it, plus one new pin on the two-leg sentence. Split from the first commit of this branch because the defect was only visible in a live patrol run taken after it — the self-test pins the clause's substrings and both readings are legal English. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr --- scripts/pm/check-half-states.mjs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 54a78fcf364..c98dfdaed61 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -16039,8 +16039,9 @@ export function summaryLine(counts, findingCount) { `Untimestamped readings (H44): ${counts.readingComments ?? 0} comment(s) across ` + `${counts.readingThreads ?? 0} thread(s) ALREADY in hand were read for the five artefact shapes, ` + `and the seat leg widened the fetch to ${counts.readingSeatRead ?? 0} of ` + - `${counts.readingSeatCandidates ?? 0} HELD seat post(s) — every held seat, whatever its lane ` + - 'spelling, which is wider than H32\'s own population and deliberately so. A seat post\'s window is ' + + `${counts.readingSeatCandidates ?? 0} seat post(s) — every HELD seat whatever its lane spelling, ` + + 'PLUS the TRIAGE post whether or not it is held, which is wider than H32\'s own population in both ' + + 'directions and deliberately so. A seat post\'s window is ' + `its NEWEST comment page, not the oldest one a page-less request returns: ${counts.readingSeatNewest ?? 0} ` + 'of those post(s) had that page LOCATED from the carrier\'s own `comments` count and the rest fell ' + 'back to page 1, one request per seat post either way and shared with H56, H64 and H65. ⛔ A verdict posted on a ' + @@ -29078,7 +29079,12 @@ Mutual exclusion: \`get_comments\` page 747 → \`[]\`, page 746 = my own R+117 t('H44: no code is left unregistered by this change', familyRegistryCoverage().missing.length, 0); t('H44: every count key rides the enumerated forwarding contract', ['readingThreads', 'readingComments', 'readingSeatCandidates', 'readingSeatRead'].every((k) => SWEEP_COUNT_KEYS.includes(k)), true); t('H44 summary: the corpus pair is reported', saidBy('h44Readings', summaryLine({ readingComments: 411, readingThreads: 37 }, 0)).includes('411 comment(s) across 37 thread(s)'), true); - t('H44 summary: …and the widened seat leg with it', saidBy('h44Readings', summaryLine({ readingSeatRead: 10, readingSeatCandidates: 10 }, 0)).includes('10 of 10 HELD seat post(s)'), true); + // ⚠️ REWRITTEN (#18324), not deleted: this pin read `'10 of 10 HELD seat + // post(s)'` while the leg's population was held seats alone. The triage post + // is in it now whatever its holder, so a clause still saying HELD would have + // described the count wrongly on every run — the count itself is unchanged. + t('H44 summary: …and the widened seat leg with it', saidBy('h44Readings', summaryLine({ readingSeatRead: 10, readingSeatCandidates: 10 }, 0)).includes('10 of 10 seat post(s)'), true); + t('H44 summary: …and the clause names BOTH legs, so the count is readable', saidBy('h44Readings', summaryLine({ readingSeatRead: 10, readingSeatCandidates: 10 }, 0)).includes('every HELD seat whatever its lane spelling, PLUS the TRIAGE post whether or not it is held'), true); t('H44 summary: the PR residual is declared on EVERY run, not just interesting ones', saidBy('h44Readings', summaryLine({}, 0)).includes('PULL REQUEST is NOT in this corpus'), true); t('H44 summary: …and says the counts are a lower bound', saidBy('h44Readings', summaryLine({}, 0)).includes('LOWER BOUND'), true); t('H44 summary: a bare line renders numbers, never `undefined`', saidBy('h44Readings', summaryLine({}, 0)).includes('undefined'), false);