Uh oh!
There was an error while loading. Please reload this page.
H40: dangling-reference patrol over the live board's # references (report-only) - #13964
Merged
Merged
Conversation
…erences Sweeps the open board's bodies and already-cached comment threads for `#` references into this repo, resolves each DISTINCT number once, and reports the ones that fail to resolve with their referrers. Report-only. ⛔ closed != unreachable: only HTTP 404 is read as unresolvable; a closed card resolves normally and is never a finding, and any other failure is UNJUDGED rather than a vanished card. Renders as a RESERVED section plus an unconditional summary clause, so the anchor body's row trim cannot silently drop it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2
os-sam
marked this pull request as ready for review
August 31, 2026 20:02
os-sam
enabled auto-merge
August 31, 2026 20:02
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#13634 — A-half only, per triage's narrowing (comment 5477064578). The
ruling-durability half was split out and is neither answered nor waited on here.
Adds H40, a report-only row family in
scripts/pm/check-half-states.mjs: itsweeps the live board for
#references into this repo, resolves each DISTINCTnumber once, and reports the ones that fail to resolve together with the cards
that point at them.
The four hard requirements, and where each one lives
① Sweep bodies and comments; resolve each distinct number once; report failures
with their referrers.
referencedNumbersextracts,buildReferenceIndexmapseach distinct number to every referrer that names it,
h40ResolutionPlandecideswhat gets asked, and the report names each dangling number with its referrers
(capped at
H40_REFERRER_LIST_CAP). One resolution per number, cached across thewhole sweep — numbers H19 already resolved are reused rather than re-fetched.
② closed is not unreachable.
h40Verdicthas four states, not two:resolvable— silent, foreverunresolvable— the findingunjudged— reported as unjudged, never as a vanished cardnot attempted— counted and announced, never read as cleanMeasured control, 2026-08-31, one session and one token: #13398#13399#13178
#12924#13456 all answered HTTP 404, while #13324#13571 (closed) and #13592 (a
closed PR) resolved normally alongside four open cards. Both verdicts present on
the same instrument, so the failures are facts about those numbers rather than
about the channel. Widening
unresolvablepast 404 is the one change that wouldturn this into an accusation generator, and the self-test pins it in both
directions.
③ Report-only. No reference is rewritten, no card is closed, no label is
written, and ⛔ no cause is asserted for any unresolvable number — the row hands a
human two facts and stops. The finding text says so, and so does the summary line.
④ Cost control. The referrer corpus costs zero extra requests: it is the
open cards and PRs this sweep already holds plus the comment threads already in
its cache. Resolutions are bounded by
H40_RESOLUTION_BUDGET(400), spentnewest-first, with the remainder published as NOT ATTEMPTED and the floor it
reached stated by number.
The budget is a wall-clock bound derived from two measurements taken here, not a
guess: ~366 ms per resolution, and a baseline sweep of 228 s against
half-state-patrol.yml'stimeout-minutes: 15. The live run below came in at360 s — about 40% of that cap.
Two cheaper shapes were measured and declined, and the header records both so
nobody re-derives them. A bulk
state=all&sort=created&direction=asclistingmaps 100 consecutive numbers per request, and cannot work: the issues listing
refuses deep pagination — page 99 answers 200 and page 100 answers HTTP 422,
so an ascending pass stops at #9,900 while this repo is past #13,900, and the
descending half that would cover the rest reorders under every new issue (a row
shifting across a page boundary mid-pass would be published as a dangling
reference — a false accusation about a live card). A batched GraphQL query was
declined structurally: this file's whole transport story is REST-shaped, so a
second transport would run outside the prerequisite classifier in the one script
whose contract is that an unread input never reads as a clean one.
The renderer-trimming interaction (#13947), and the choice made
That card measured the anchor body dropping 157 of 231 rows by position in one
flat list, so a new family added as ordinary finding rows can be computed and
then silently omitted. Chosen: a reserved section, plus an unconditional
summary clause — not a priority position and not a deferral.
Reasoning, in the file's own conventions: a row family whose entire subject is a
defect that no instrument reports must not itself be droppable. The reservation
is the mechanism
renderTriggerIndex, the H39 census and the rate premisealready use — the block's length is charged to the budget before finding rows
are laid out, so the trim can only ever fall on rows that announce their own
omission. A priority position was rejected because it is probabilistic (the loud
and UNJUDGED bands already compete there), and marking confirmed findings as
UNJUDGED to buy trim protection would corrupt a band whose meaning the file
depends on. The section keeps its own
H40_ROW_CAPso the reservation staysbounded, and the summary clause renders above the fold on every run — which
matters because H40's findings deliberately do not enter the finding COUNT.
Three self-test cases pin this directly: a 900-row flood that really does trigger
the trim, the H40 section still rendered inside it, and the body still under
MARKDOWN_BODY_BUDGET.One deliberate divergence from the neighbouring extractors
Fenced code blocks are stripped; inline code spans are kept. Every other
extractor here blanks spans because GitHub's closing-keyword parser does not fire
inside one, so a backticked keyword is genuinely inert. That reason does not
transfer: a reference is a pointer a reader follows, and a backticked
Blocked-by: #9612is the natural markdown for a line meant to be grepped. Aseat that opens a backticked number and gets a resolution error has hit exactly
the defect this row exists to find. Fences still go, for H17's reason — a number
inside one is a citation, not a pointer.
Evidence
Self-test: 1826 → 1922 cases, all green (
pnpm check:pm-half-states).Ablation — three legs, each mutated, proven on disk (anchor uniqueness, an
injected/removed text count, and a
git hash-objectdiffering from HEAD) andrestored back to the byte-identical HEAD blob with
git diff HEADempty. Nobuild step sits between this source file and its suite — the suite runs the file
directly — so there is no rebuild leg to state, and the mutation proof stands in
its place. Control on the unmutated tree: green, 1922 pass.
h40Verdicttreats any failure as unresolvableLive run at this head, 360 s: 45 numbers referred to by the live board do
not resolve, 0 unjudged, over 3,725 distinct references read off 421 open
cards/PRs and 293 already-cached comment threads; 1,019 answered free from
listings in hand, 400 attempted (355 answered), 2,306 not attempted at the budget
(reached down to #11410). The known instances appear with named referrers —
#13456 via #13440#13536#13634#13766, #13398 via #6021#12981#13634#13766 —
alongside 41 that nothing had reported. Four of the new ones were re-resolved by
hand (#13462#13412#13165#12924, all 404) and one referrer claim was verified
against the referring body (#13417 does name #13412 and #13410), so the row is
not reporting an extractor artefact.
Read coupling:
pnpm check:pm-clause2-carriersgreen (64 cases) — everyexport it imports is untouched.
check-governed-mergesandci-failureself-tests green too.
Gates, derived on the final head
35c684c7vianode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(19commands: 17 by path, 2 by change kind — this edits a gate script). All green,
plus
pnpm lintover the whole repo (78 s, exit 0) andcheck:nul-bytes. Fourof the 19 are NOT MEASURED locally rather than red, each by its own printed
verdict:
check-test-completenesssays so in as many words (it grades a savedturbo log that no local run produces) and
check-partof-closing-keywordneedsPR context it only has in CI. The other two were PREREQUISITE NOT MET before
pnpm installand green after it.Scope
scripts/pm/check-half-states.mjsonly — no other file, no workflow change. Thescheduled patrol picks the row up on its next fire with no wiring. Code-only, so
skip-changeset: nothing is published from any package.#13947 remains open and is not addressed here; the reservation above is how this
row avoids its hazard, not a fix for it. #13766 is out of scope: it carries the
B-half question and is ruled separately.
Generated by Claude Code