Skip to content

Commit 02e2838

Browse files
committed
pm(patrol): re-derive H8/H22/H23's window arithmetic against the measured merge rate (#11118)
H8's docblock justified a two-page window with "~18 merges/day", a figure the repo had outrun more than sevenfold. The sentence claimed a reach "well past the longest measured unexecuted-verdict latency" (~11 days at that rate); the window it described had become 1.78 days. Nothing could notice, because the arithmetic was prose. Re-measured 2026-08-23T08:42:15Z, windows pinned as full ISO instants per the card's approxidate warning: rate 300 commits, 2026-08-21T04:00:19Z..2026-08-23T08:22:47Z = ~137.5/day (300/300 carrying the squash marker; agrees with the card's ~132/day) H8 2 pages = 1.78d (oldest merge 2026-08-21T14:00:28Z) 4 pages = 2.96d (oldest merge 2026-08-20T09:41:02Z) -> widened H22 2 pages = 0.65d of update-recency (~15.6 HOURS, ~2.6 sweeps) 4 pages = 1.70d -> widened H23 3 pages = 2.18d, ~8.7 sweeps -> unchanged H8 is widened rather than restated because its damage model is asymmetric: the residue it reports is the paired write nobody noticed, which correlates with age, so the population most likely to age out is the population the row is for. H22's surprise is the divisor — ordered by `updated`, its rows are consumed by issue ACTIVITY, not closures, and the rate is bursty in exactly the direction that ejects fresh residue while residue is being produced fastest. Four pages restores what its anti-drowning argument was choosing (a couple of days) and does not reopen the deep tail it refuses (500+ carriers spanning months). H23 kept its cap: it is the one window that DERIVED its number from a measured rate, and the only one that survived a re-measure. The derivation is now executable — `windowCoverageDays`, `sweepOverlap`, `MEASURED_MERGES_PER_DAY`, `PATROL_CADENCE_HOURS` — so a cap and the sentence justifying it cannot drift apart again. Cost: four extra requests per sweep, four sweeps a day, against a 15,000/h quota. Self-test 977 -> 997 cases, including the degenerate inputs that must answer "cannot divide" rather than Infinity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
1 parent 1604623 commit 02e2838

1 file changed

Lines changed: 165 additions & 12 deletions

File tree

‎scripts/pm/check-half-states.mjs‎

Lines changed: 165 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5077,16 +5077,103 @@ async function listOpenPullRequests() {
50775077
}
50785078

50795079
/**
5080-
* The merged-PR window H8 reads: most recently UPDATED closed PRs, merged
5081-
* ones only, capped at two pages — a quota decision whose consequence is
5082-
* H8's stated boundary (a delivery older than the window is invisible). At
5083-
* ~18 merges/day two pages reach well past the longest measured
5084-
* unexecuted-verdict latency; `sort=updated` so a long-lived PR that merges
5085-
* late is still in the window when it matters.
5080+
* How this file states a bounded window's boundary, in ONE place (#11118).
5081+
*
5082+
* Every window here is a page cap, and a page cap is meaningless until it is
5083+
* divided by a rate. The three windows below used to state their boundaries in
5084+
* prose, each derived from a rate measured whenever that item was written — and
5085+
* one of them (H8's) was still quoting `~18 merges/day` from a repo that had
5086+
* since accelerated more than sevenfold. The sentence justifying the cap was
5087+
* describing an ~11-day reach for a window that had become ~1.8 days, and
5088+
* nothing in the file said so, because nothing in the file could: the
5089+
* arithmetic was prose, so no reader and no test could ever disagree with it.
5090+
*
5091+
* These two helpers make the derivation executable instead. The self-test pins
5092+
* them; the docblocks below quote numbers these functions produce.
5093+
*
5094+
* @param {number} rows — the window's size in rows (pages × per_page).
5095+
* @param {number} ratePerDay — the measured production rate of those rows.
5096+
* @returns {number|null} days of coverage, or null when the rate cannot divide.
50865097
*/
5098+
export function windowCoverageDays(rows, ratePerDay) {
5099+
if (!Number.isFinite(rows) || !Number.isFinite(ratePerDay) || ratePerDay <= 0) return null;
5100+
return rows / ratePerDay;
5101+
}
5102+
5103+
/** How many consecutive patrol runs see a given row before it ages out. */
5104+
export function sweepOverlap(coverageDays, cadenceHours = PATROL_CADENCE_HOURS) {
5105+
if (!Number.isFinite(coverageDays) || !Number.isFinite(cadenceHours) || cadenceHours <= 0) {
5106+
return null;
5107+
}
5108+
return (coverageDays * 24) / cadenceHours;
5109+
}
5110+
5111+
/** The scheduled patrol's period — `cron: '37 1,7,13,19 * * *'` in the workflow. */
5112+
export const PATROL_CADENCE_HOURS = 6;
5113+
5114+
/**
5115+
* The default-branch merge rate, MEASURED — the divisor every window below
5116+
* uses, and the number the stale `~18/day` was replaced with.
5117+
*
5118+
* Window pinned as full ISO INSTANTS, deliberately: `git log --since=` is an
5119+
* approxidate that fills the time-of-day from *now*, and two runs twelve
5120+
* minutes apart returned 1,443 and 1,441 messages for what read as one window
5121+
* (#11118's own warning, which this re-derivation obeys rather than repeats).
5122+
*
5123+
* read 2026-08-23T08:42:15Z, `GET /repos/{repo}/commits`, 3 pages
5124+
* window 2026-08-21T04:00:19Z … 2026-08-23T08:22:47Z (2.18 days)
5125+
* rows 300 commits, 300 of them carrying the `(#N)` squash marker
5126+
* rate 300 / 2.18 = ~137.5 merges/day
5127+
*
5128+
* `main` is linear (measured on the same corpus at #10942's filing: 1,975
5129+
* reachable = 1,975 first-parent, 0 merge commits), so the commit count and the
5130+
* merge count are one count. The figure agrees with the independent 2026-08-22
5131+
* measurement this card was filed on (1,546 commits in 11.7 days ≈ 132/day),
5132+
* which is what makes it a rate rather than a spike.
5133+
*/
5134+
export const MEASURED_MERGES_PER_DAY = 137.5;
5135+
5136+
/**
5137+
* The merged-PR window H8 reads: most recently UPDATED closed PRs, merged ones
5138+
* only, capped at four pages — a quota decision whose consequence is H8's
5139+
* stated boundary (a delivery older than the window is invisible).
5140+
* `sort=updated` so a long-lived PR that merges late is still in the window
5141+
* when it matters.
5142+
*
5143+
* ## The boundary, re-derived (#11118)
5144+
*
5145+
* The cap was two pages, justified by a sentence claiming they "reach well past
5146+
* the longest measured unexecuted-verdict latency" — true at ~18 merges/day,
5147+
* which is where that sentence came from, and false at the measured 137.5.
5148+
* Both readings, taken 2026-08-23T08:42:15Z over the live endpoint:
5149+
*
5150+
* 2 pages = 200 rows -> 197 merged, oldest merge 2026-08-21T14:00:28Z = 1.78d
5151+
* 4 pages = 400 rows -> 397 merged, oldest merge 2026-08-20T09:41:02Z = 2.96d
5152+
*
5153+
* (Derivation and reading agree: `windowCoverageDays(400, 137.5)` = 2.91d. The
5154+
* merged-only filter costs almost nothing — 397 of 400 closed PRs in the window
5155+
* were merged — so rows and merges are interchangeable here in practice.)
5156+
*
5157+
* Four pages is chosen over an honest restatement because this row's damage
5158+
* model is asymmetric in the direction that punishes a short window: H8 reports
5159+
* a card whose delivering PR merged while the card still says `pm:dispatched`,
5160+
* i.e. precisely the paired write NOBODY noticed — which correlates with age.
5161+
* The population most likely to age out is the population the row exists for.
5162+
* The measured H8 specimen (#11036) sat unreported ~22h, so a 1.78-day window
5163+
* left the row about 2x its own worst measured latency; 2.96 days restores the
5164+
* "comfortably past it" the docblock always claimed. Cost: two extra requests
5165+
* per sweep, four sweeps a day, against a 15,000/h core quota.
5166+
*
5167+
* At the 6-hourly cadence that is `sweepOverlap(2.96)` ≈ 11.8 consecutive runs
5168+
* that see a given merge — the window is a detection HORIZON, not a retry
5169+
* budget: past it the finding is not delayed, it is gone (H22 catches the part
5170+
* of that population whose card later closes; nothing catches the rest).
5171+
*/
5172+
export const MERGED_WINDOW_PAGES = 4;
5173+
50875174
async function listRecentlyMergedPullRequests() {
50885175
const out = [];
5089-
for (let page = 1; page <= 2; page++) {
5176+
for (let page = 1; page <= MERGED_WINDOW_PAGES; page++) {
50905177
const batch = await rest(
50915178
`/repos/${OWNER_REPO}/pulls?state=closed&sort=updated&direction=desc&per_page=100&page=${page}`,
50925179
);
@@ -5111,10 +5198,39 @@ async function listRecentlyMergedPullRequests() {
51115198
* a live duty someone remembers — and the deep tail is a backfill question,
51125199
* not a patrol question. `state=closed` is the ONLY closed-issue read in this
51135200
* file; every other collector stays open-only by construction.
5201+
*
5202+
* ## The boundary, re-derived — and the surprise in it (#11118)
5203+
*
5204+
* "Recent" was never measured here; it was assumed to mean roughly what H8's
5205+
* window meant. It did not, and the divisor is the reason: this window is
5206+
* ordered by `updated`, and a closed card is BUMPED by every later comment,
5207+
* label write and cross-reference, so the rows are consumed by issue ACTIVITY
5208+
* rather than by closures. Read 2026-08-23T08:42:15Z over the live endpoint:
5209+
*
5210+
* 2 pages = 200 rows -> updated 2026-08-22T17:05:44Z … 2026-08-23T08:39:14Z
5211+
* = 0.65d (~15.6 HOURS of update-recency)
5212+
* 4 pages = 400 rows -> updated 2026-08-21T15:44:49Z … 2026-08-23T08:39:14Z
5213+
* = 1.70d
5214+
*
5215+
* At 6-hourly runs the old cap gave `sweepOverlap(0.65)` ≈ 2.6 consecutive
5216+
* sweeps — and the derived floor is tighter still (200 rows / ~308 updates/day
5217+
* ≈ 1.30d for four pages against the 1.70d measured), because the rate is
5218+
* BURSTY: a triage round that touches a few hundred closed cards can eject a
5219+
* fresh residue card inside one cadence, and it does so exactly when residue is
5220+
* being produced fastest. That correlation is what makes 2.6 sweeps thin rather
5221+
* than merely small.
5222+
*
5223+
* Four pages restores what the anti-drowning argument above was actually
5224+
* choosing — a couple of days of recent residue — rather than the fifteen hours
5225+
* it turned out to be buying. It does NOT reopen the deep tail: the population
5226+
* that argument refuses is the 500+ historical carriers spanning months, and
5227+
* 1.7 days is not in it. Cost: two extra requests per sweep.
51145228
*/
5229+
export const CLOSED_ISSUE_WINDOW_PAGES = 4;
5230+
51155231
async function listRecentlyClosedIssues() {
51165232
const out = [];
5117-
for (let page = 1; page <= 2; page++) {
5233+
for (let page = 1; page <= CLOSED_ISSUE_WINDOW_PAGES; page++) {
51185234
const batch = await rest(
51195235
`/repos/${OWNER_REPO}/issues?state=closed&sort=updated&direction=desc&per_page=100&page=${page}`,
51205236
);
@@ -5148,9 +5264,16 @@ async function listRecentlyClosedIssues() {
51485264
* Measured over the corpus above: 1,546 commits in 11.7 days ≈ 132/day. Three
51495265
* pages ≈ 300 commits ≈ 2.3 days, against a patrol that fires every 6 hours —
51505266
* roughly a 9× overlap, so a message has to survive nine consecutive sweeps to
5151-
* age out unseen. (The `~18 merges/day` figure in `listRecentlyMergedPullRequests`
5152-
* predates that acceleration; nothing here depends on it, but a future reader
5153-
* re-deriving a window from it should re-measure first.)
5267+
* age out unseen.
5268+
*
5269+
* RE-MEASURED 2026-08-23T08:42:15Z and unchanged, which is why this window
5270+
* alone kept its cap while H8's and H22's were widened (#11118): the same three
5271+
* pages read 300 commits spanning 2026-08-21T04:00:19Z … 2026-08-23T08:22:47Z
5272+
* = 2.18 days at ~137.5/day, i.e. `sweepOverlap(2.18)` ≈ 8.7 runs. This item's
5273+
* docblock was the only one that DERIVED its cap from a measured rate instead
5274+
* of quoting a remembered one, and it is the only one that survived contact
5275+
* with a re-measure — the argument for keeping the derivation executable
5276+
* (`windowCoverageDays`, `MEASURED_MERGES_PER_DAY`) rather than in prose.
51545277
*
51555278
* No `sha=` parameter: the endpoint defaults to the repository's own default
51565279
* branch, which keeps this reader repo-agnostic exactly like every other listing
@@ -5162,7 +5285,7 @@ async function listRecentlyClosedIssues() {
51625285
* commits would simply feed this row a few branch-side messages, which are a
51635286
* surface GitHub's parser reads too — wider, never wrong.
51645287
*/
5165-
const COMMIT_WINDOW_PAGES = 3;
5288+
export const COMMIT_WINDOW_PAGES = 3;
51665289

51675290
async function listRecentDefaultBranchCommits() {
51685291
const out = [];
@@ -8292,6 +8415,36 @@ function selfTest() {
82928415
t('vocabulary: a fresh awaiting park is still clean', h11ImportantParked(parkedCard(['bug', AWAITING_MAINTAINER_LABEL], { created: daysAgo(2) }), NOW), null);
82938416
t('vocabulary: an UNimportant awaiting card is not inventory', h11ImportantParked(parkedCard([AWAITING_MAINTAINER_LABEL]), NOW), null);
82948417

8418+
// -- The window arithmetic (#11118) ----------------------------------------
8419+
// The derivation is executable so that a cap and the sentence justifying it
8420+
// cannot drift apart again: H8's docblock quoted `~18 merges/day` while the
8421+
// repo ran at ~132, which turned a claimed ~11-day reach into ~1.8 days with
8422+
// nothing able to notice.
8423+
t('windows: coverage is rows / rate', windowCoverageDays(400, 100), 4);
8424+
t('windows: H8 four pages against the measured rate', Number(windowCoverageDays(MERGED_WINDOW_PAGES * 100, MEASURED_MERGES_PER_DAY).toFixed(2)), 2.91);
8425+
t('windows: …which is what the live reading measured (2.96d), within a rounding', windowCoverageDays(MERGED_WINDOW_PAGES * 100, MEASURED_MERGES_PER_DAY) > 2.5, true);
8426+
t('windows: the OLD two-page cap was under two days, not the ~11 its prose claimed', Number(windowCoverageDays(200, MEASURED_MERGES_PER_DAY).toFixed(2)), 1.45);
8427+
t('windows: …and the stale ~18/day figure is what produced the ~11-day claim', Number(windowCoverageDays(200, 18).toFixed(1)), 11.1);
8428+
t('windows: H23 keeps three pages', COMMIT_WINDOW_PAGES, 3);
8429+
t('windows: …and its re-measured coverage still clears two days', windowCoverageDays(COMMIT_WINDOW_PAGES * 100, MEASURED_MERGES_PER_DAY) > 2, true);
8430+
t('windows: H22 widened to four pages', CLOSED_ISSUE_WINDOW_PAGES, 4);
8431+
t('windows: H8 widened to four pages', MERGED_WINDOW_PAGES, 4);
8432+
// The cadence side: a window is a detection HORIZON, and the overlap says how
8433+
// many runs see a row before it is gone for good.
8434+
t('windows: the patrol cadence is the workflow\'s', PATROL_CADENCE_HOURS, 6);
8435+
t('windows: overlap is coverage over cadence', sweepOverlap(1, 6), 4);
8436+
t('windows: H8\'s new window is seen by ~12 runs', Math.round(sweepOverlap(windowCoverageDays(400, MEASURED_MERGES_PER_DAY))), 12);
8437+
t('windows: H22\'s OLD 0.65d reading was ~2.6 runs (the thin one)', Number(sweepOverlap(0.65).toFixed(1)), 2.6);
8438+
t('windows: H22\'s measured new reading is ~6.8 runs', Number(sweepOverlap(1.7).toFixed(1)), 6.8);
8439+
// Degenerate inputs answer "cannot divide" rather than Infinity or NaN — a
8440+
// number no reading produced must never be renderable as a boundary.
8441+
t('windows: a zero rate cannot divide', windowCoverageDays(200, 0), null);
8442+
t('windows: a negative rate cannot divide', windowCoverageDays(200, -5), null);
8443+
t('windows: a missing rate cannot divide', windowCoverageDays(200, undefined), null);
8444+
t('windows: a missing row count cannot divide', windowCoverageDays(undefined, 137.5), null);
8445+
t('windows: a zero cadence has no overlap', sweepOverlap(2, 0), null);
8446+
t('windows: an unusable coverage has no overlap', sweepOverlap(null), null);
8447+
82958448
// -- H26: a block whose target can never close, + the stale chain (#11219) --
82968449
// The measured cards, by name, and both directions of every leg.
82978450
const waiting = (number = 1119) => ({

0 commit comments

Comments
 (0)