diff --git a/.github/workflows/half-state-patrol.yml b/.github/workflows/half-state-patrol.yml index ae3e427aa3..acc7895b11 100644 --- a/.github/workflows/half-state-patrol.yml +++ b/.github/workflows/half-state-patrol.yml @@ -11,16 +11,39 @@ name: Half-State Patrol # Every divergence from upstream is listed here, once, so a future re-sync knows # exactly what it must not clobber: # -# 1. `PM_SWEEP_CLOSED_WINDOW_PAGES: 0` on the sweep step — H22's closed-card -# reader is OFF in this install. Measured 2026-08-24: 815 closed cards here -# carry `pm:dispatched`, and ~347 of the 400 issues in upstream's window -# carry some `pm:*` residue label (~87%, against the 26% upstream measured). -# Stripping `pm:*` on close was never this lane's practice, so H22 here -# reports the CONVENTION, not a defect — ~347 rows that would consume the -# whole body budget and trim every other predicate out of the anchor. The -# sweeper says "UNREAD, not clean" in its summary rather than reporting 0. -# ⛔ Re-enabling is a BACKFILL decision (strip the residue first), never a -# quiet default flip. The script's default is still upstream's 4. +# 1. `PM_SWEEP_CLOSED_FLOOR` on the sweep step — H22's closed-card reader is +# ON here, but judges only cards closed on/after the cutover date. It read +# `PM_SWEEP_CLOSED_WINDOW_PAGES: 0` (reader fully OFF) until 2026-08-28. +# +# The measurement that forced the hold, taken 2026-08-24: 815 closed cards +# here carry `pm:dispatched`, and ~347 of the 400 issues in upstream's +# window carry some `pm:*` residue label (~87%, against the 26% upstream +# measured). At that density H22 reports the CONVENTION, not a defect — +# ~347 rows that consume the whole body budget and trim every other +# predicate out of the anchor. +# +# objectui#5985 recorded that as a two-way choice: either stripping is the +# rule (and ~815 closed cards need a BACKFILL first) or the row is simply +# not wanted here. The dated floor is the third option both readings omit. +# `pm:*` on a card closed before the convention existed is inert history — +# the dispatch loop reads state on OPEN cards only — so judging from the +# cutover forward buys the row's whole value (residue produced from now +# on, while the paired write is still a live duty) at zero backfill and +# zero historical noise. ⛔ NO bulk label backfill of closed cards was run +# and none is owed; the sweeper writes no label under any code path. +# +# The cutover is 2026-08-28, the date the strip-on-close convention was +# written into the pm-dispatch protocol's label-discipline section. ⛔ Do +# not move it EARLIER without re-measuring: every day it moves back pulls +# in cards closed under no convention at all. `resolveClosureFloor` in the +# sweeper refuses a malformed value (exit 2) rather than degrading to "no +# floor", because a silent degrade here restores the 87% flood four times +# a day and a flooded anchor reads exactly like a working patrol. +# +# ⚠️ The floor is UPSTREAM code, not a fourth hand divergence: a re-sync +# that replaces the sweeper with upstream's keeps it. What diverges is +# this WIRING — upstream sets no floor, because its own board measured 26% +# and it treats recent closed residue as a live duty. # 2. `scripts/invoked-as.mjs` is in the `paths:` filter below — the sweeper # imports it, and it was ported alongside. # 3. `scripts/pm/check-half-states.mjs` carries `DEFAULT_SWEEP_REPO = @@ -209,11 +232,14 @@ jobs: # workflow — the two agree by construction and a copy of this file # cannot end up sweeping the repo it was copied FROM. PM_SWEEP_REPO: ${{ github.repository }} - # objectui#5791 — H22's closed-card reader is OFF here. See divergence - # (1) in this file's header for the measurement that decided it, and - # `resolveClosedWindowPages` in the sweeper for what 0 means. The - # rendered body reports the surface as UNREAD, never as clean. - PM_SWEEP_CLOSED_WINDOW_PAGES: '0' + # objectui#5985 — H22's closed-card reader is ON, with a DATED FLOOR: + # only cards closed on/after this date are judged. See divergence (1) + # in this file's header for the measurement and the reasoning, and + # `resolveClosureFloor` in the sweeper for the parsing and the loud + # refusal. The page window is deliberately absent: it is back to the + # script's own upstream default of 4, and the floor — not a zeroed + # window — is what keeps the ~815 historical carriers out. + PM_SWEEP_CLOSED_FLOOR: '2026-08-28' PROVENANCE: >- run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) · commit `${{ github.sha }}` · trigger `${{ github.event_name }}` diff --git a/scripts/__tests__/check-half-states.test.ts b/scripts/__tests__/check-half-states.test.ts index 0b3da5cd25..a4968401b2 100644 --- a/scripts/__tests__/check-half-states.test.ts +++ b/scripts/__tests__/check-half-states.test.ts @@ -11,6 +11,7 @@ import { DEFAULT_SWEEP_REPO, h22ClosedCardPmResidue, resolveClosedWindowPages, + resolveClosureFloor, resolveSweepRepo, summaryLine, } from '../pm/check-half-states.mjs'; @@ -81,17 +82,28 @@ describe('check-half-states — sweeps THIS board (objectui#5791 adaptation)', ( }); }); -describe('check-half-states — H22 closed-card reader is OFF here (objectui#5791)', () => { +describe('check-half-states — H22 runs here behind a DATED CLOSURE FLOOR (objectui#5985)', () => { /** - * The measurement that decided this, re-measured 2026-08-24 for the port: - * 815 closed cards in objectui carry `pm:dispatched`, and ~347 of the 400 + * ⚠️ REPLACED PIN, not a respelled one. Until 2026-08-28 this block pinned the + * opposite arrangement: the reader fully OFF via `PM_SWEEP_CLOSED_WINDOW_PAGES: + * '0'`, under a ruling that stripping `pm:*` on close was not this lane's + * convention. That ruling is superseded — fleet practice now strips `pm:*` in + * the landing/close stroke, and the convention is written into the pm-dispatch + * protocol's label-discipline section as of 2026-08-28. + * + * The measurement that forced the original hold still stands and is why the + * re-enable is FLOORED rather than plain (re-measured 2026-08-24 for the + * port): 815 closed cards here carry `pm:dispatched`, and ~347 of the 400 * issues inside upstream's window carry some `pm:*` residue label (~87%, - * against the 26% upstream measured on its own board). Stripping `pm:*` on - * close was never this lane's practice, so H22 here reports the CONVENTION - * rather than a defect — ~347 rows that would exhaust the body budget and - * trim every other predicate out of the anchor on day one. + * against the 26% upstream measured on its own board). An unfloored re-enable + * would report the CONVENTION rather than defects — ~347 rows that exhaust + * the body budget and trim every other predicate out of the anchor on day one. + * + * ⛔ The floor is what makes the re-enable cheap: no backfill of the 815 + * historical carriers was run, and none is owed. The sweeper writes no label + * under any code path, so no bulk rewrite is even reachable from here. */ - it('keeps upstream\'s default in the script, so the port stays a straight copy', () => { + it('keeps upstream\'s page default in the script, so the port stays a straight copy', () => { // The divergence lives in the WORKFLOW, not in the script's default. This // is what lets the predicate file be re-synced verbatim. expect(CLOSED_ISSUE_WINDOW_PAGES).toBe(4); @@ -99,45 +111,91 @@ describe('check-half-states — H22 closed-card reader is OFF here (objectui#579 expect(resolveClosedWindowPages({}).source).toBe('default'); }); - it('is switched off by the workflow, and visibly so', () => { - expect(workflow).toMatch(/PM_SWEEP_CLOSED_WINDOW_PAGES: '0'/); - expect(resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: '0' }).pages).toBe(0); - expect(resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: '0' }).valid).toBe(true); + it('is switched ON by the workflow — the page-window hold is GONE', () => { + // The hold's absence is asserted directly. Left in place beside a floor it + // would win silently (0 pages reads nothing whatever the floor says), and + // the anchor would keep reporting UNREAD while looking re-enabled. + expect(workflow).not.toMatch(/^\s*PM_SWEEP_CLOSED_WINDOW_PAGES:/m); + expect(resolveClosedWindowPages({}).pages).toBe(4); + }); + + it('sets a dated floor in the workflow, and visibly so', () => { + expect(workflow).toMatch(/PM_SWEEP_CLOSED_FLOOR: '2026-08-28'/); + const resolved = resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }); + expect(resolved.valid).toBe(true); + expect(resolved.floor?.toISOString()).toBe('2026-08-28T00:00:00.000Z'); }); it('refuses a malformed page count instead of silently defaulting', () => { - // Silently falling back to 4 would re-open the reader this install shut, - // and the anchor would carry the residue as though someone chose that. + // The knob still exists and still refuses garbage — it is simply not what + // holds the historical residue out any more. for (const raw of ['O', '-1', '1.5', 'four']) { expect(resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: raw }).valid).toBe(false); } expect(resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: ' 2 ' }).pages).toBe(2); }); - it('leaves the H22 predicate itself untouched', () => { - // The adaptation is a window, not a rewritten rule: handed a closed card - // with residue the predicate must still say so. Re-enabling the reader is - // therefore a one-variable decision, not a code change. + it('refuses a malformed floor instead of silently running unfloored', () => { + // ⛔ The property the re-enable turns on. A floor that degraded to "no + // floor" would put ~347 convention rows in the anchor four times a day, + // and a flooded anchor reads exactly like a working patrol. + for (const raw of ['28-08-2026', '2026/08/28', 'yesterday', '2026-8-28', 'O']) { + expect(resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: raw }).valid).toBe(false); + expect(resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: raw }).floor).toBeNull(); + } + // …including a shape-valid date that does not exist. `Date.parse` rolls + // `2026-02-31` to March rather than rejecting it, so the resolver + // round-trips the parse instead of trusting it. + expect(resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-02-31' }).valid).toBe(false); + // Unset is valid and means "no floor" — upstream's own behaviour. + expect(resolveClosureFloor({}).valid).toBe(true); + expect(resolveClosureFloor({}).floor).toBeNull(); + }); + + it('applies the floor to the H22 predicate: old closures out, new ones judged', () => { + // The whole re-enable, in two assertions. The old card is the ~815-card + // backlog in miniature — real residue, deliberately NOT a finding. const closedCard = { state: 'closed', state_reason: 'completed', labels: [{ name: 'pm:dispatched' }], }; - expect(h22ClosedCardPmResidue(closedCard)).toContain('`pm:dispatched`'); - expect(h22ClosedCardPmResidue({ ...closedCard, state: 'open' })).toBeNull(); + const floor = resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).floor; + expect(h22ClosedCardPmResidue({ ...closedCard, closed_at: '2026-01-01T00:00:00Z' }, floor)).toBeNull(); + expect(h22ClosedCardPmResidue({ ...closedCard, closed_at: '2026-08-29T00:00:00Z' }, floor)).toContain('`pm:dispatched`'); + // The cutover date itself is judged: it is the first day the convention + // applies, so cards closed within it are the convention's own population. + expect(h22ClosedCardPmResidue({ ...closedCard, closed_at: '2026-08-28T12:00:00Z' }, floor)).toContain('`pm:dispatched`'); + // Unfloored, the predicate is byte-for-byte upstream's — the port stays a + // straight copy and the floor is opt-in. + expect(h22ClosedCardPmResidue({ ...closedCard, closed_at: '2026-01-01T00:00:00Z' })).toContain('`pm:dispatched`'); + expect(h22ClosedCardPmResidue({ ...closedCard, state: 'open' }, floor)).toBeNull(); + }); + + it('names the floor in the summary, so a floored pass cannot read as a full one', () => { + // ⛔ #4690, in the shape this change could newly break: "H22 read 200" with + // a floor silently applied overstates what was judged. The line must carry + // the floor date, and must say the earlier closures are UNJUDGED rather + // than clean. + const counts = { repo: 'objectstack-ai/objectui', issues: 3, unscoped: 4, prs: 1, merged: 2, closed: 200 }; + const floored = summaryLine({ ...counts, closedFloor: '2026-08-28' }, 0); + expect(floored).toContain('H22 read 200 recently-closed issue(s)'); + expect(floored).toContain('only cards closed on/after 2026-08-28 are judged'); + expect(floored).toContain('NOT a reading about them'); + // An unfloored pass must not grow the clause. + expect(summaryLine(counts, 0)).not.toContain('are judged'); }); - it('reports the closed surface as UNREAD, never as clean', () => { - // ⛔ The property the whole adaptation turns on (#4690). A disabled reader - // and an empty result are the same number and opposite facts; if this ever - // renders "H22 read 0", the anchor starts asserting a clean closed surface - // that nothing looked at. + it('still reports a DISABLED reader as UNREAD, never as clean', () => { + // The disabled branch is no longer wired here, but it is still live code + // and still the property the port turned on (#4690): a disabled reader and + // an empty result are the same number and opposite facts. Kept pinned so + // re-adding the hold cannot quietly render "H22 read 0". const counts = { repo: 'objectstack-ai/objectui', issues: 3, unscoped: 4, prs: 1, merged: 2, closed: 0 }; const disabled = summaryLine({ ...counts, closedWindowDisabled: true }, 0); expect(disabled).toContain('is DISABLED in this install'); expect(disabled).toContain('UNREAD'); expect(disabled).not.toContain('H22 read 0'); - // …and it names the way back, so the choice is reversible by a reader. expect(disabled).toContain('PM_SWEEP_CLOSED_WINDOW_PAGES'); // The other direction: an enabled reader that found nothing IS a clean diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 40828ca31f..394b8ebfe6 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -3658,9 +3658,30 @@ export const PM_RESIDUE_LABELS = [ * predicate cannot double-report the population every other item already reads. * That gate is the predicate's own, not the caller's, because it is the one * thing separating this row from a restatement of H3. + * + * `floor` is the optional dated closure floor (see `resolveClosureFloor`): a + * `Date` before which a closed card is out of scope, or null for "judge every + * card in the window", which is the default and upstream's own behaviour. + * + * ⚠️ A card whose `closed_at` cannot be read is judged, NOT skipped. The floor + * is a scope decision that needs a date to make; without one the card's + * position relative to the cutover is UNKNOWN, and silently dropping it would + * narrow the pass on unread data — #4690 in the direction this file refuses + * everywhere else. The listing endpoint always carries `closed_at` on a closed + * issue, so fail-open costs no noise in practice; it just keeps the one + * unreadable card visible instead of disappeared. + * + * @param {any} issue + * @param {Date | null} [floor] */ -export function h22ClosedCardPmResidue(issue) { +export function h22ClosedCardPmResidue(issue, floor = null) { if (issue?.state !== 'closed') return null; + if (floor) { + const closedAt = Date.parse(issue.closed_at ?? ''); + // Strictly BEFORE the floor is out of scope; a card closed ON the cutover + // date is the first day the convention applies and is judged. + if (!Number.isNaN(closedAt) && closedAt < floor.getTime()) return null; + } const residue = labelNames(issue ?? {}).filter((l) => PM_RESIDUE_LABELS.includes(l)); if (residue.length === 0) return null; const list = residue.map((l) => `\`${l}\``).join(', '); @@ -4414,7 +4435,7 @@ export function isLoudFinding(message) { * restartCandidates?: number, blockerResolved?: number, * blockerTargets?: number, commits?: number, commitBindings?: number, * commitBindingMessages?: number, - * closedWindowDisabled?: boolean }} counts + * closedWindowDisabled?: boolean, closedFloor?: string }} counts * @param {number} findingCount */ export function summaryLine(counts, findingCount) { @@ -4471,7 +4492,9 @@ export function summaryLine(counts, findingCount) { 'not clean (see `resolveClosedWindowPages` and `PM_SWEEP_CLOSED_WINDOW_PAGES` in ' + '`.github/workflows/half-state-patrol.yml`). ' : `H22 read ${counts.closed ?? 0} recently-closed issue(s) for \`pm:*\` state residue (bounded window; ` + - `older closed carriers are outside it by design). `) + + `older closed carriers are outside it by design` + + `${counts.closedFloor ? `, and only cards closed on/after ${counts.closedFloor} are judged — ` + + 'earlier closures predate the strip-on-close convention and are NOT a reading about them' : ''}). `) + `H23 read ${commits} squash commit message(s) from the default branch's recent window, carrying ` + `${commitBindings} closing-keyword binding(s) across ${commitBindingMessages} message(s) ` + `(bounded window; a message that landed before it is invisible by design). ` + @@ -5559,6 +5582,78 @@ export function resolveClosedWindowPages(env = {}) { const CLOSED_WINDOW = resolveClosedWindowPages(process.env); +/** + * H22's DATED CLOSURE FLOOR — the cutover date at and after which a closed + * card's `pm:*` residue is judged (objectui#5985). + * + * ## The dilemma this dissolves, and why THIS install is the one that needed it + * + * The window above is bounded by UPDATE recency, which is the wrong axis for + * the question this repo kept running into: "was this card closed under the + * convention, or before it existed?" Measured here 2026-08-24, while porting + * this file: 815 closed cards carry `pm:dispatched`, and ~347 of the 400 + * issues in the window carry some `pm:*` residue (~87%, against the 26% + * upstream measured on its own board). At that density H22 reports the + * CONVENTION rather than a defect — ~347 rows that exhaust the anchor body + * budget and trim every other predicate's findings out of the report. So the + * reader shipped OFF (`PM_SWEEP_CLOSED_WINDOW_PAGES: '0'`, divergence 1), and + * objectui#5985 recorded the choice as a two-way one: either stripping is the + * rule (and ~815 cards need a BACKFILL before H22 can be honest) or it is not + * (and H22 is simply not a predicate this repo wants). + * + * The floor is the third option both readings omit. `pm:*` on a card closed + * before the convention was written is inert history: nothing queries it as a + * claim of in-flight-ness, because the loop reads state on OPEN cards only + * (`is:open` is in every one of its inventory queries). Judging only cards + * closed on/after a cutover date therefore buys the row's whole value — the + * residue produced from now on, while the paired write is still a live duty + * someone remembers — at zero backfill and zero historical noise. ⛔ The + * alternative this file must never grow is a bulk label rewrite of closed + * cards: 815 mutating writes to make a report quieter is machinery serving the + * instrument, and no code path here writes a label at all. + * + * ## Ported, not invented here + * + * This is upstream's code (objectstack `scripts/pm/check-half-states.mjs`), + * carried across with the docblock re-pointed at this board's measurement. ⚠️ + * It is NOT a fourth hand divergence: a re-sync that replaces this file with + * upstream's must keep the floor, because upstream has it. What IS divergent + * is the WIRING in `.github/workflows/half-state-patrol.yml` — see divergence + * 1 there, which now sets a floor instead of switching the reader off. + * + * ## Malformed is REFUSED, never defaulted + * + * A typo'd floor that silently became "no floor" would restore the 87% flood + * here, four times a day, and the flood reads as a working patrol — the same + * trap `resolveSweepRepo` and `resolveClosedWindowPages` refuse by name. Only + * the `YYYY-MM-DD` spelling is accepted: a bare `Date` parse would take + * "yesterday-ish" strings and timezone-bearing ones whose midnight is not the + * one the workflow author meant, and the value is written by hand exactly once. + */ +export function resolveClosureFloor(env = {}) { + const raw = String(env.PM_SWEEP_CLOSED_FLOOR ?? '').trim(); + if (!raw) return { floor: null, source: 'default', valid: true, raw: '' }; + if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) { + return { floor: null, source: 'PM_SWEEP_CLOSED_FLOOR', valid: false, raw }; + } + const at = Date.parse(`${raw}T00:00:00Z`); + if (Number.isNaN(at)) return { floor: null, source: 'PM_SWEEP_CLOSED_FLOOR', valid: false, raw }; + // A shape-valid string can still name a date that does not EXIST, and + // `Date.parse` does not reject all of them: `2026-13-01` is NaN (the month + // is outside the ISO range) but `2026-02-31` silently ROLLS to 2026-03-03. + // So the parse is round-tripped rather than trusted. Letting a rolled date + // through would move the floor days past where its author wrote it and, + // worse, do it silently — the floor is the one input here whose whole job is + // to say which cards were judged. + const floor = new Date(at); + if (floor.toISOString().slice(0, 10) !== raw) { + return { floor: null, source: 'PM_SWEEP_CLOSED_FLOOR', valid: false, raw }; + } + return { floor, source: 'PM_SWEEP_CLOSED_FLOOR', valid: true, raw }; +} + +const CLOSED_FLOOR = resolveClosureFloor(process.env); + async function listRecentlyClosedIssues() { // 0 pages = the closed reader is off in this install. Returning early (rather // than letting the loop not execute) keeps the intent legible and makes it @@ -5891,9 +5986,10 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // line can say what this pass covered on its own terms. for (const issue of await listRecentlyClosedIssues()) { seenClosed.set(issue.number, issue); - const residue = h22ClosedCardPmResidue(issue); + const residue = h22ClosedCardPmResidue(issue, CLOSED_FLOOR.floor); if (residue) findings.push([issue, 'H22', residue]); } + stats.closedFloor = CLOSED_FLOOR.raw; // H23 — the commit-message surface (#10942). The counting is not incidental: // this row's measured yield is ~6 in 1,546, so a silent H23 is the normal @@ -6900,9 +6996,82 @@ function selfTest() { t(`H22: \`${label}\` on a closed card is residue`, typeof h22ClosedCardPmResidue(closedCard([label])), 'string'); } + // -- H22's DATED CLOSURE FLOOR (objectui#5985) ------------------------------ + // + // The floor is what lets THIS install re-enable the row without the backfill + // its own card thought was the only alternative: judge cards closed on/after + // the cutover date, leave the ~815 historical carriers unjudged, write no + // labels at all. The cases below pin the three properties that decision rests + // on — the floor is HONOURED, its absence changes nothing, and a malformed + // value is refused rather than silently becoming "no floor". + const FLOOR = new Date(Date.parse('2026-08-28T00:00:00Z')); + const closedOn = (labels, closed_at) => ({ ...closedCard(labels), closed_at }); + + // Honoured, both directions. The old card is the ~815-card backlog in + // miniature: it carries real residue and is deliberately NOT a finding. + t('H22 floor: a card closed BEFORE the floor is out of scope', h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-08-01T09:00:00Z'), FLOOR), null); + t('H22 floor: …however much residue it carries', h22ClosedCardPmResidue(closedOn(['pm:dispatched', 'pm:queue', 'pm:blocked'], '2026-01-01T00:00:00Z'), FLOOR), null); + t('H22 floor: a card closed AFTER the floor is judged', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-08-29T09:00:00Z'), FLOOR), 'string'); + t('H22 floor: …and the row still names the residue label', h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-08-29T09:00:00Z'), FLOOR).includes('`pm:dispatched`'), true); + // The boundary is inclusive: the cutover date is the first day the convention + // applies, so a card closed within it is the convention's own population. + t('H22 floor: a card closed ON the floor date is judged', typeof h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-28T00:00:00Z'), FLOOR), 'string'); + t('H22 floor: …and later the same day too', typeof h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-28T23:59:59Z'), FLOOR), 'string'); + t('H22 floor: one second before the floor is out', h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-27T23:59:59Z'), FLOOR), null); + // The floor narrows scope; it never invents findings. + t('H22 floor: a clean card after the floor is still clean', h22ClosedCardPmResidue(closedOn(['domain:ui'], '2026-08-29T09:00:00Z'), FLOOR), null); + t('H22 floor: the closed gate still outranks the floor', h22ClosedCardPmResidue({ ...issue(['pm:dispatched']), state: 'open', closed_at: null }, FLOOR), null); + // Fail-OPEN on an unreadable closure date: the floor cannot be applied, so + // the card stays visible rather than being dropped on unread data (#4690). + t('H22 floor: a card with no closed_at is judged, not dropped', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], null), FLOOR), 'string'); + t('H22 floor: …and an unparseable one likewise', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], 'not-a-date'), FLOOR), 'string'); + + // Floor ABSENT — upstream's default, and the property that makes the ported + // code a no-op for an install that does not set the variable. + t('H22 floor: absent floor judges an old closed card exactly as before', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-01-01T00:00:00Z')), 'string'); + t('H22 floor: …an explicit null is the same as omitting it', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-01-01T00:00:00Z'), null), 'string'); + t('H22 floor: …and a clean old card is still clean', h22ClosedCardPmResidue(closedOn(['domain:ui'], '2026-01-01T00:00:00Z'), null), null); + + // resolveClosureFloor — the env reading, including the loud refusal. + t('closure floor: unset means no floor', resolveClosureFloor({}).floor, null); + t('closure floor: …and that is a VALID reading, not an error', resolveClosureFloor({}).valid, true); + t('closure floor: …reported as the default source', resolveClosureFloor({}).source, 'default'); + t('closure floor: whitespace is unset too', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: ' ' }).floor, null); + t('closure floor: a YYYY-MM-DD date resolves to UTC midnight', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).floor.toISOString(), '2026-08-28T00:00:00.000Z'); + t('closure floor: …and is valid', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).valid, true); + t('closure floor: …and names its source', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).source, 'PM_SWEEP_CLOSED_FLOOR'); + t('closure floor: surrounding whitespace is trimmed, not rejected', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: ' 2026-08-28 ' }).valid, true); + // Malformed is REFUSED. Each of these would otherwise become "no floor" and + // restore the ~347-row flood this install shut off. + for (const bad of ['28-08-2026', '2026/08/28', 'yesterday', '2026-08-28T00:00:00Z', '2026-8-28', 'O', '0']) { + t(`closure floor: \`${bad}\` is refused, not defaulted`, resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: bad }).valid, false); + } + // …including a well-SHAPED date that does not exist — the case a bare regex + // would pass and whose floor would exclude every card, rendering an empty + // H22 as a clean closed surface. + t('closure floor: a shape-valid impossible date is refused', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-02-31' }).valid, false); + t('closure floor: …and an impossible month likewise', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-13-01' }).valid, false); + t('closure floor: a refused value carries no floor to fall back on', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: 'yesterday' }).floor, null); + t('closure floor: …and is reported as itself for the error message', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: 'yesterday' }).raw, 'yesterday'); + // The floor and the page window are INDEPENDENT knobs: a 0-page window still + // reads nothing whatever the floor says, and that is the disabled summary, + // not a floored one. Pinned because the workflow now sets the floor INSTEAD + // of the 0, and a future reader must not read one as an alias of the other. + t('closure floor: the floor does not switch the reader on', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: '0', PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).pages, 0); + t('closure floor: …and the window does not set a floor', resolveClosureFloor({ PM_SWEEP_CLOSED_WINDOW_PAGES: '4' }).floor, null); + // The summary line's H22 clause — a pass that read nothing must not read the // same as a board with no residue (#4690), so the count is always stated. t('summary: the H22 clause states what the closed pass read', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200 }, 0).includes('H22 read 200 recently-closed issue(s)'), true); + // …and when a floor is in force the line SAYS so: "read 200" with a floor + // silently applied would overstate what was judged, which is the same + // unread-reads-as-clean defect the count itself exists to prevent. + t('summary: a floored pass names the floor date', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200, closedFloor: '2026-08-28' }, 0).includes('only cards closed on/after 2026-08-28 are judged'), true); + t('summary: …and says the earlier closures are not a reading about them', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200, closedFloor: '2026-08-28' }, 0).includes('NOT a reading about them'), true); + t('summary: an unfloored pass adds no floor clause', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200 }, 0).includes('are judged'), false); + // The DISABLED branch still wins over a floor: a 0-page window read nothing, + // so the line must keep saying UNREAD rather than describing a floored pass. + t('summary: a disabled reader with a floor set still reads UNREAD', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closedWindowDisabled: true, closedFloor: '2026-08-28' }, 0).includes('UNREAD'), true); t('summary: an absent closed count degrades to 0, never to undefined', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0 }, 0).includes('H22 read 0 recently-closed'), true); // -- H23: the COMMIT-MESSAGE surface (#10942) ------------------------------- @@ -9115,6 +9284,18 @@ if (isMain) { ); process.exit(2); } + // A malformed closure floor is the same class and gets the same answer. It + // must not degrade to "no floor": this is the install whose closed surface is + // ~87% residue, so a silent default would flood the anchor four times a day + // and the flood renders as a working patrol. + if (!process.argv.includes('--self-test') && !CLOSED_FLOOR.valid) { + console.error( + `check-half-states: ${CLOSED_FLOOR.source}=${JSON.stringify(CLOSED_FLOOR.raw)} is not a ` + + '`YYYY-MM-DD` date. Refusing to fall back to an unfloored closed pass — on this board, ' + + 'no floor is a report about the convention rather than about defects.', + ); + process.exit(2); + } if (process.argv.includes('--self-test')) { selfTest(); } else if (process.argv.includes('--probe')) {