Commit e08fae8
fix(pm): the card-comment read pages to a declared cap, so a thread past 100 is UNJUDGED rather than a truncated claim pool (#18799)
Fixes #18683
Clause-②: no
## The defect
`scripts/pm/check-clause2-carriers.mjs` read a card's comment thread
with ONE request — `/issues/{n}/comments?per_page=100`, no `page=`
ladder, no short-read check — while the two sibling list reads in the
same file paged to a declared cap and answered `null` (UNJUDGED, never
clean) when they hit it. One file, two OPPOSITE defaults on "I did not
read everything", and the fail-OPEN one was the read that arbitrates
OWNERSHIP: the governing-claim pool, its membership, and the `Clause-②`
declaration read out of it all come from those rows. A thread past 100
comments handed the pool its first page and nothing said the tail had
been dropped, so a claim written past row 100 was not superseded — it
was never a candidate — and a superseded carrier governed in its place.
## The before-reading, on a 101-row fixture
Driven end to end through the real CLI against a stubbed board
(`--pair`, no network), on `main` `d9ba33df4c` (script blob
`d753e2a8cf06d8f72c436e0a1917b2fecb8be813`). Two fixtures, both 101
rows, both differing from a complete thread only past the page boundary.
| fixture | BEFORE (`main`) | AFTER (this PR) |
|---|---|---|
| 100 claim-free rows, the 101st the only `Claim:` | `card-comments: 100
row(s)` · `claim.selected: none — no comment on this thread carries a
line beginning \`Claim:\`` · **exit 4, row C2 `absent`** |
`card-comments: 101 row(s)` · the 101st claim is the pool ·
`claim.clause2-line: DECLARED \`no\`` · **exit 0** |
| row 1 an older `Claim:` declaring `yes`, the 101st a newer one
declaring `no` | `claim.clause2-line: DECLARED \`yes\`` from the
SUPERSEDED carrier, which is not even listed as rejected · **exit 4, row
C3** | the newer claim governs, the older is listed REJECTED/SUPERSEDED
· `DECLARED \`no\`` · **exit 0** |
The second row is the fail-OPEN direction stated as a measurement: one
thread, two readings, and they disagree on the declaration itself.
## The ladder, and the cap
All three list reads now go through one `pagedListRead` helper — it
pages to a declared cap, stops on the FIRST short page (no wasted
request), and on the cap files the one shared `pageCapNote` sentence and
answers `null`. `readCarrierEvents` (`EVENT_PAGE_CAP` 10) and
`readPullFiles` (`FILE_PAGE_CAP` 3) keep their caps to the number; what
they gain is that the third read can no longer hold a different default.
`COMMENT_PAGE_CAP` is **10** pages = 1,000 comments. Sized on this
board, read 2026-09-17 off the open-issue list rows (550 rows listed,
cross-checked against `open_issues_count` = 550):
- longest open thread of any kind: seat post #6015 at **895** comments —
nine pages;
- next four: #12708 at 365, #6023 at 241, #6017 at 206, #6024 at 187;
seat post #7623 at 71;
- longest thread carrying a queue label: #13799 at **117** (`pm:queue`,
p2, unassigned);
- longest card in the clause-② population (28 pairs the sweep derived
that day): #17534 at **14**.
So ten pages clears the whole board today with a page to spare, and it
is the same ten `EVENT_PAGE_CAP` uses — a reader comparing two caps in
one file should have to remember one number.
## The input record
The diagnosis key stays `comments`, so every sentence already keyed to
it still finds its diagnosis. Two declared fields are added to
`INPUT_RECORD_PAIR_FIELDS`, one per thread this file reads:
```
pair.1.card-comments: 101 row(s)
pair.1.card-comment-pages: 2 of 10 page(s) requested — the ladder stopped on a SHORT page, so the thread is COMPLETE
pair.1.pr-comment-pages: 1 of 10 page(s) requested — the ladder stopped on a SHORT page, so the thread is COMPLETE
```
and, when the cap is what stopped the read:
```
pair.1.card-comment-pages: CAPPED — 10 of 10 page(s) of 100 comments each were requested and EVERY ONE came back full, so the tail is past the cap and the thread is UNREAD (UNJUDGED) — ⛔ never a truncated pool, ⛔ never a clean reading
```
A thread of exactly 100 rows and a thread whose tail was dropped are the
same `100 row(s)` in every other line the block prints; they differ
here, because the complete one stopped on a short page and the truncated
one did not stop at all. The request ledger PR #18681 added shows the
same ladder from the other side — request #3 is now
`…/comments?per_page=100&page=1` and #4 is `&page=2`.
## The pins
A new `--self-test` battery, `#18683: the card-comment read pages to a
cap — past 100 is UNJUDGED, ⛔ never a truncated pool`, 27 cases,
declared in `SELF_TEST_BATTERIES` with the roster floor raised 29 → 30.
It drives the ladder with an offline page server that reproduces
GitHub's own semantics and counts the requests; ⛔ no network. What it
holds: the 101st claim ENTERS the pool and GOVERNS, and its line is what
the limb reads; the same thread cut at 100 reads `absent` (the CONTROL —
the reading the un-paged read produced); the newer claim past the
boundary supersedes the older one inside it, and cut at 100 the
superseded carrier's `yes` is what the limb reads; a capped read is
`null`, which is neither `missing` nor `absent` nor a carrier but
`unreadable`; the ladder stops on the first short page (2 requests for
101 rows, 1 for a short thread, 2 for exactly 100 — a full page is
indistinguishable from a finished one); a page that came back unread
ends the ladder and the record says the cap was NOT what stopped it; the
input record declares and prints both ladder fields; the sibling caps
are untouched; and all three reads render ONE cap sentence.
## The census, and the triage's upgrade probe
Report-only, no state write. Over the 550 open rows (521 issues, 29 PRs)
read on 2026-09-17:
- open `pm:queue` / `pm:dispatched` cards: **274**, of which **1**
exceeds 100 comments — #13799 at 117;
- all open issues over 100 comments: **8** — #6015 (895), #12708 (365),
#6023 (241), #6017 (206), #6024 (187), #6021 (145), #6367 (127), #13799
(117). Seven are `pm:seat` posts;
- open PRs over 100 comments: **0**; the longest is #18638 at 10.
**The upgrade probe's result: the condition is NOT met today.** The
clause-② population is what a `--pair`/sweep derivation actually pairs,
not what carries a queue label: the sweep derived **28 pairs from 29
open PRs**, and the longest card thread among them is **14** rows
(#17534). The two open PRs that mention a 100+-comment card in prose —
#18786 (#6015, #7623) and #18765 (#6024) — deliver #18693 and #18652
respectively, both under 10 comments; driven live before and after, both
answer exit 0 with an identical pair reading. So no recorded `--pair`
verdict on this board today was taken on a truncated pool, and the
triage's p1 condition (「找到任一进入条款②认领池、评论数 > 100 的卡并驱动一次」) has no live
instance to drive. The exposure is one PR away rather than realised:
#13799 is `pm:queue` at 117 and enters the population the moment a PR
delivers it.
The cost is unchanged by the ladder, measured on the same board: **64
reads for 28 pairs, before and after**, because every live thread fits
one page and the ladder stops on a short page. The live `--pair 18765`
input records differ in exactly three lines — the two request paths
gaining `&page=1`, and the two new ladder fields.
1 parent ac720a9 commit e08fae8
1 file changed
Lines changed: 273 additions & 39 deletions
0 commit comments