Repository navigation
check-half-states: H22's closed-card pass is TRUNCATED at the 12-page quota ceiling on this board, so the closed-residue census is silently short #16217
Description
Activity
Claim: PM loop round 8 (continued) — flight N, one pure-code card: H22's closed-card pass in
scripts/pm/check-half-states.mjsis a 3-day CLOSURE horizon paged byupdated_atunder a 12-page quota ceiling, and on this board the ceiling bites before the horizon, so the closed-residue census is silently short. Ruled: the pass pages until the oldestupdated_aton a page is older than the horizon, under a hard cap of 40 pages; the census line always prints the horizon actually reached as a date, and says TRUNCATED only when the hard cap bit; the self-test pins the three shapes (horizon reached under the cap, cap bites first, an empty board). Unblocked by PR #16202 (flight C) merging at 15:35Z.
Session:session_019RfFHiRCSs3JXLK4cwcfox
Branch:claude/issue-16217-h22-closed-pass-horizon
Worktree:objectstack-issue-16217
Domain:domain:skills
File surface:scripts/pm/check-half-states.mjs(the H22 closed-card leg and its self-test cases; no new rows, family markers untouched). ⛔ No other file.
Container & model: XS,mode:subagent, opus —dispatch-gates.mjs --tier: no path-derived mandate onscripts/pm/**; a window policy in one leg.
Clause-②: no
Serial: no open PR touchescheck-half-states.mjs(PR #16202 merged 15:35Z; flight L edits the ratchet map, not this file). Pure code ⇒ in-seat review, then the seat flips ready and arms SQUASH auto-merge;skip-changeset. The PR carriesFixes #16217.
Generated by Claude Code
os-dev-report
{ "issue": 16217, "status": "done", "branch": "claude/issue-16217-h22-closed-pass-horizon", "pr": "https://github.com/objectstack-ai/objectstack/pull/16391", "premise_still_valid": true, "summary": "H22's closed-card pass in scripts/pm/check-half-states.mjs now pages to its 3-day CLOSURE horizon under a hard 40-page cap (was 12), and its census clause prints the horizon actually REACHED as a date on every run. The horizon-driven stop was already correct and is unchanged (select on closed_at, page on updated_at via the shared pageExhaustsWindow); the defect was the backstop in front of it, sized from a divisor measured at depth while the pages a 3-day horizon costs come out of the fastest part of the stream. Re-measured at depth 2026-09-06: 4,000 rows reach 12.8 days, so 40 is ~4.3x headroom, and the docblock says in terms that a future bind means narrowing the stream, not raising the number again. New pure readings oldestPageStamp / reachedDate feed stats.closedReachedAt, which rides SWEEP_COUNT_KEYS; describeWindowBound takes an optional `reached` that prints beside the day count in BOTH directions and, by coupling rather than a second flag, retires this clause's 'the ceiling needs raising' remedy. Callers that cannot state a reach keep that sentence, so H23's clause and the open-listings clause are byte-identical and two cases pin it. The pinned divisor MEASURED_CLOSED_ISSUE_UPDATES_PER_DAY is NOT overwritten (H8's rule); the stale '~6 pages for 3 days' cost note now states the disagreement instead. One file, one leg, no new rows, family markers and PM_RESIDUE_LABELS untouched, no second constant.", "tests": "SELF-TEST: node scripts/pm/check-half-states.mjs --self-test -> exit 0, verdict line 'check-half-states self-test: 2588 cases pass.' Case count BEFORE 2553 (origin/main f377394ae2, measured by swapping the base file in and running it), AFTER 2588 -> +35. selfTest() became async (one call site) so the three shapes are driven through the REAL pager: listRecentlyClosedIssues takes its page reader as a parameter, so the cases pin the loop the sweep runs rather than a re-typed copy. SHAPES PINNED: (1) horizon reached under the cap -> 3 pages, truncated=false, closedReachedAt dated, only in-horizon closures admitted (200 of 300), clause says 'boundary reached'; (1b, the teeth) a stream whose horizon sits on page 13 -> 13 pages, truncated=false, i.e. the run the old 12 could not finish; (2) cap bites first -> 40 pages, truncated=true, clause carries '⛔ TRUNCATED', names the 40-page ceiling, states the reach anyway, and no longer says 'the ceiling needs raising'; (3) empty board -> 0 rows, 1 page (the cost of learning the board is empty; the family's page counter is COST, so 1 not 0 — reported rather than reshaped), truncated=false, closedReachedAt null, clause invents no date and still discloses its bound. ABLATION (no build step: this is a plain .mjs run directly, nothing resolves through dist, so no rebuild leg exists and none is claimed). Each mutation was confirmed ON DISK before reading anything — grep counts of the removed text (0) and the injected text (1) plus git hash-object differing from the HEAD blob; a non-landing mutation was made to print 'reading VOID' and it did fire once on a mangled anchor, which was then redone. Each leg restored with git checkout HEAD -- ABSOLUTE_PATH under an EXIT/INT/TERM trap and verified byte-identical (blob back to 584efeb022956d6f4729086a057c052f8744b77e, git diff HEAD empty). Results: closedReachedAt never assigned -> 5 red; ceiling put back to 12 -> 3 red; the clause's reach fragment emptied -> 2 red. LIVE, read-only, same board and same instant, pre-change file swapped in and restored byte-identically around the reading. objectstack BEFORE: \"H22's closed-card window is a TIME cap of 3 day(s), read in 12 page(s) — ⛔ TRUNCATED at the 12-page quota ceiling BEFORE reaching that horizon, so this pass is a page cap and a card that closed earlier than what those pages reached is invisible; the ceiling needs raising.\" objectstack AFTER: \"H22's closed-card window is a TIME cap of 3 day(s), reaching back to 2026-09-03, read in 13 page(s) (boundary reached: paging ran past the horizon, not merely the first N rows).\" objectui AFTER: same sentence, \"reaching back to 2026-09-03, read in 6 page(s) (boundary reached ...)\". So the old ceiling was ONE page short — page 12's oldest updated_at was 2026-09-03T17:41:14Z, about an hour newer than the horizon. Neither board truncates at 40; the ruling's 'if objectstack still truncates at 40, say so' does not fire. GATES, every exit captured by redirect BEFORE any pipe: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 36 commands, all 36 run, all exit 0; --ran reconciles verbatim '✓ dispatch-gates --ran: 36 derived famil(ies) accounted for — 36 run, 0 NOT-MEASURED.' Beyond the derivation, all exit 0: check:pm-clause2-carriers (it imports this module), check:pm-skill-id-lint, check:pm-label-desc-cap, check:pm-governed-prose, check:pm-skill-ratchet, check:engine-double-contract, check:console-injection, check:i18n-stale-fill, check-published-list-mirrors. check:nul-bytes is inside the 36; the diff was also scanned by hand for control bytes and carries none, and it contains no SKILL.md line citations and no model identifiers. check-governed-merges --test scripts/pm/check-half-states.mjs, verbatim: 'governed-surface predicate: 0 of 1 path(s) hit the register (5 surfaces, repo-agnostic). ✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.' check:pm-skill-id-lint cannot reach this file, CHECKED not recalled: its corpus is SCAN_ROOT '.claude/skills/pm-dispatch' plus EXTRA_FILES ['.claude/agents/os-dev.md','AGENTS.md'], .md only. Whole-repo pnpm lint (eslint . --no-inline-config) through scripts/pm/os-verify-lock.sh slot issue-16217, acquired first try: 'VERDICT command-exit 0 · held the lock 110s (1m50s) · waited 0s' — the whole farm ran, no narrowing to declare. TWO NOT MEASURED, not red: check:published-readme-exports and check:dts-closure both exit 3 PREREQUISITE NOT MET because no package has a dist/ in this worktree; both need a whole-repo build a one-file scripts/pm diff cannot affect, and CI runs them after its build. The 36-gate battery, the lint and the live sweeps all ran at the FINAL commit 680df51ecc; nothing was committed after them. Staleness reading: origin/main moved to 0374bcba9f after the battery and two files the derivation reads changed in that range (scripts/check-partof-closing-keyword.mjs and its workflow); the derived command list is byte-identical either way and CI evaluates the merge result, so CI runs the current checker. CI, polled in the foreground on head 680df51eccf391acd7d28c05244392f83cd31808 until every run completed (this card's dispatch asks for the wait): 35 check runs, ALL COMPLETED, NON-GREEN none — 'Lint & Repo Gates' success (the whole gate farm, including the two artefact rosters that were NOT MEASURED locally for want of a dist/), 'Test Core' 1..6/6 success, all four Type Check jobs success, 'Check Changeset' success (skip-changeset applied additively and confirmed by a comparison read-back: read-back ['size/m','skip-changeset'], nothing in union(read, target) missing — size/m was added concurrently by the size labeler, not by me), 'Live half-state sweep' success, 'Governed Surface Queue Guard' success, 'Part-of PR must not also close its card' success, 'No other open PR may claim the same issue' and '...the same single-writer path' both success. The rest are skipped by path filter.", "mcp_calls": "0 — every GitHub read and write this run went through repo-scoped REST (probed green at the top of the run); zero MCP GitHub calls", "open_questions": [], "out_of_scope_findings": [ "noted, not filed (below the #16351 threshold, PM's call to promote): H22's pinned divisor MEASURED_CLOSED_ISSUE_UPDATES_PER_DAY = 188.3 (2026-08-31) is now drifted — the board measured ~312/day averaged over 40 pages and ~414/day over the leading 12, i.e. a factor of 1.66 to 2.2. H8 carries a mechanical staleness alarm for exactly this shape (observedMergeRatePerDay + classifyRatePremise + RATE_PREMISE_BAND, which fires past a factor of 2 or past the re-measure horizon); H22 carries the same kind of pinned premise with NO alarm, so its divisor can drift silently and the next author sizing the cap from it repeats this card. This PR closes the acute half in prose only — the cost note now states the disagreement instead of quietly editing it — and deliberately does not overwrite the constant (H8's rule: checked against a sweep, never updated by one) nor widen the diff to a second mechanism. The contract-first fix is H8's alarm applied to H22's divisor." ] }
Generated by Claude Code
Observed while running the live half-state sweep read-only, twice, as the verification for the flight on #16036 / #16170 / #16037 (PR #16202). Not addressed there — that flight's ruling scoped its diff to one file and three named readers, and this is a window policy in the same file's H22 leg.
The measurement
Both
objectstack-ai/objectstacksweeps (2026-09-06, ~05:20Z and ~05:35Z) printed, verbatim:So the row DECLARES its own shortfall, in its own words, on every run — and the remedy sentence ("the ceiling needs raising") is already written. What is missing is a card that carries it, which is why the state has persisted through however many patrol runs have printed it.
The clause also explains the mechanism, and it is worth quoting because it is the reason a bigger page budget is genuinely needed rather than merely nice: the row selects on
closed_atwhile paging is bounded byupdated_at, so the stream is consumed by closed-issue ACTIVITY (~188.3/day, measured 2026-08-31) rather than by closures (~11/day). A 3-day CLOSURE horizon therefore costs many more than 3 days' worth of closure pages.objectstack-ai/objectuiin the same session did not print the truncation clause, so this is a board-volume effect rather than a universal one — the ceiling is adequate on a quiet board and not on this one.What it costs
H22 is the closed-card
pm:*state-residue census. Under the 2026-08-31 ruling it files no row at all, but it is COUNTED into the sweep's census line, so a truncated pass makes the census silently short: a card that closed earlier than the pages reached is invisible, and nothing in the summary distinguishes "no residue" from "not read". That is the shape this file treats as worse than a finding.Deliberately NOT proposed here, because the trade is a request budget and that is the owning lane's call: whether the remedy is a higher page ceiling, a narrower
updated_atfilter, aclosed_at-ordered listing where the API supports it, or a coverage clause that reports the reached horizon as a date so a short pass is legible rather than merely declared.Dedup
The complete open enumeration was read via the REST list endpoint and reconciled against
open_issues_count: 680 rows read (646 issues + 34 PRs) againstopen_issues_count680, exact. Firing controls on the same corpus: " the " 665, "claim" 253. Grepped forquota ceiling/page cap/H22/closed-card window/closed-issue ACTIVITY: 3 hits, all read — #9857 (the patrol anchor, which is the generated view CARRYING this sentence rather than a card about it) and #7623 (a seat post). Neither is this card.Refs (not addressed here): #16036, PR #16202.