Skip to content

feat(ledger): screen open items whose subject already exists on main (BACKLOG #1426) - #806

Open
wshallwshall wants to merge 3 commits into
mainfrom
worktree-agent-a3ad59f7d79723872
Open

feat(ledger): screen open items whose subject already exists on main (BACKLOG #1426)#806
wshallwshall wants to merge 3 commits into
mainfrom
worktree-agent-a3ad59f7d79723872

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

What this adds

scripts/docs/subject_exists_screen.py — the first ledger check that reads the code rather than the ledger. For each OPEN item it extracts the concrete code-side subjects the row names (commit shas, merged pull requests, file paths, distinctive symbol names) and asks git whether they are already on origin/main.

It reports candidates and flips nothing. A wrongly-closed item is invisible forever, so the closing act stays a person reading each row. There is no --fix.

Why

On 2026-09-03 five items were dispatched as builds. Two were already complete, and a Builder was spent on each discovering it.

item what had landed why the ledger did not say so
#1040 all three branch commits were ancestors of main; its cited PR was on main a note said the banner was left open for an archive pass, while an older re-score beneath it still called the landed work outstanding
#1229 its escape limb shipped 2026-08-22 in 3c5cb9885 that commit's subject names BACKLOG #1268, not #1229, so a search on the item number never finds it. The re-score calling it unbuilt is dated 2026-08-20, two days before the merge

Common shape: a re-score dated before the landing, with nothing afterward reading the code. Every existing screen reads the ledger, and the #1234 amendment already says why that cannot close it.

Positive control result

The controls run first, unconditionally, on every invocation, and they have both arms.

  • Structural control (probes): a known commit resolves and an all-zero sha does not; a tracked path resolves and an invented one does not; parse_items is found on the ref and an invented symbol is not. Failure exits 2 and prints CONTROL FAILED -- THE SCREEN IS BROKEN, NOT THE LEDGER CLEAN. Passing today.
  • Ledger control: #1229 and #1040 both FIRED as candidates, each on a sha-ancestor-landing. Failure to fire exits 1. When either is closed the control retires by name rather than passing silently — a control that stopped applying must not read like one that passed.
  • Extractor control: a synthetic row modelled on both cases must yield all 8 subjects across 4 kinds, and must not read a bare #999 as a pull request. A negative arm asserts prose naming nothing yields nothing.
  • tests/test_subject_exists_screen.py adds the controls' own negative arms — test_the_probe_control_fails_against_a_probe_that_cannot_say_no and test_the_extractor_control_fails_when_a_subject_kind_is_lost. Without those the controls would be one-sided instruments.

Sample run (origin/main @ 46ea10a78) — NOT acted on

items examined (OPEN only): 275
subjects extracted: sha=277, pr=108, path=2369, symbol=1027 (total 3781)
symbol probes run: 1027 (plus 2 by the control)
verdicts: 80 candidate, 114 weak-candidate, 81 no-signal
  #1229: FIRED as expected
  #1040: FIRED as expected

Strong-signal distribution: sha-ancestor-landing 80, pr-merged-landing 53, path-added-after 2.

Two rows beyond the calibration cases look like the same shape. Reported as candidates, not findings — I did not read them and this PR closes nothing:

  • #1255tests/test_conftest_name_collision_guard.py was ADDED to the ref 2026-08-26, against a row whose newest date is 2026-08-25.
  • #1276 ("generate a self-signed TLS certificate on first run") — docs/adr/0172-the-engine-always-serves-tls-minting-a-self-signed-certificate-on-first-run.md was ADDED 2026-09-02, against a row whose newest date is 2026-08-25.

Constraints honoured

  • #N is never read bare. It spells a pull request and a ledger item identically. Only BACKLOG #N (a cross-reference, never a subject) and an explicit PR #N / pull request #N are read. Pinned by three tests.
  • Ancestry is git merge-base --is-ancestor, never presence in git log output.
  • The shallow-clone trap is live, not hypothetical. This clone reports --is-shallow-repository true with 16 graft points over 931 commits reachable from origin/main. Under a graft a TRUE ancestry answer is sound and a FALSE one is not — the walk may have stopped at a boundary. Both the false and the unresolvable case become sha-unverifiable-shallow, which surfaces the item rather than returning a confident wrong answer. It fired 63 times in the sample run.
  • parse_items is imported from backlog_status_check.py, never re-derived (CLAUDE.md section 11).
  • No glyphs introduced. Section 0 tense respected — the item states no deployment axis.

Defects found by running it, both now regression-pinned

  1. The path lookbehind refused a backtick, so a path inside a code span matched as a suffix: scripts/hooks/worktree_gate.ps1 came out hooks/worktree_gate.ps1 and reported as absent. A truncation rendering as a confident negative.
  2. Same defect at a hyphen: install-git-hooks.ps1 came out git-hooks.ps1.
  3. One file named two ways (scripts/hooks/worktree_gate.ps1 and a bare worktree_gate.ps1:654) produced two signals for one fact; the screen now dedupes on the resolved path.

Ledger

  • BACKLOG #1426 allocated in this worktree and filed.
  • The Ledger erratum now records #1422 and #1425 as holes, cause: allocated by a coordinating session on a Builder's behalf, so the claim went to the coordinator's tree while the commit came from the Builder's, and the gate's worktree key and branch fallback both miss. Same class as #1297, with the variation named. Verified against the allocation registry; the coordinator's own filings (#1401 and up) are ordinary items and are called out as such.

Checks run

check result
ruff format --check / ruff check pass, both new files
mypy --strict scripts/docs/subject_exists_screen.py pass
pytest tests/test_subject_exists_screen.py 51 passed
doc guards (status, citation, dangling, link, claude-section) 179 passed
test_cp1252_console_safety test_lint_scope_parity test_tooling_partition test_doc_guards_lane 59 passed
bandit, licence header, control chars, gitleaks, actionlint, ledger gate pass (pre-commit)

Not run in this worktree, and they need reading on a runner: the full pytest suite. This venv lacks the fhir, dicom, x12, xml and webauthn extras, so pytest printed its INCOMPLETE RUN banner on every invocation — modules gated on those removed themselves at collection. I make no claim about the engine legs. windows-service-smoke and the SQL Server / Postgres legs were never reachable here either.

Open questions for whoever picks this up

  1. The 78 unread candidates. Reading them is the act that closes rows and it is deliberately not done here.
  2. Nothing schedules the screen. On a cron, at dispatch time, or on demand only — unanswered.
  3. The date proxy is the newest date anywhere in the row, because parse_items returns status and fields but not the banner block's text. Its error runs toward under-firing, which is why the two strongest signals are date-free. Reading the banner block properly would mean either extending parse_items or re-deriving its rule, and the second is forbidden.

🤖 Generated with Claude Code

…(BACKLOG #1426)

Every existing ledger gate reads the ledger. This one reads the code: for each
OPEN item it extracts commit shas, merged pull requests, file paths and symbol
names, then asks git whether they are already on origin/main.

It reports candidates and flips nothing. A wrongly-closed item is invisible
forever, so closing a row stays a person's act.

#1229 and #1040 are wired as controls: both were dispatched as builds on
2026-09-03 and both were already complete. A structural control over the probes
runs first and exits 2, so a broken screen cannot read as a clean ledger.

Ancestry uses merge-base --is-ancestor, not git log presence. A bare "#N" is
never read. This clone is shallow, so a false ancestry answer reports unknown.

First run at 46ea10a: 275 open items, 3781 subjects, 80 candidates. Unread.

Also records #1422 and #1425 as allocation holes in the Ledger erratum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wshallwshall wshallwshall added the reviewed A reviewer has read this. Removed automatically when new commits arrive. label Sep 3, 2026
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

CORRECTION FROM THE CONSOLE SEAT. The screen is good; one thing it recorded on my authority is false.

This PR records #1422 and #1425 as holes in the Ledger erratum. THEY ARE NOT HOLES. I told you they were, and your reasoning from there was sound -- you checked the registry, saw both record worktree: .../compress-claude-md-c78f90, and concluded "both keys would have missed."

That conclusion is right about YOUR tree and wrong about the numbers. The worktree key matches for the tree named in the record, which is mine. Measured by importing the module rather than reading it:

mod.Ledger(ci=False).owns("backlog", "<n>")   from compress-claude-md-c78f90

owns("backlog","1422")   = True
owns("backlog","1425")   = True
owns("backlog","999999") = False    negative control, fired

So both are still filable from the allocating tree. What the gate did to you was refuse a DIFFERENT worktree a number belonging to another tree, which is the collision it exists to prevent. "Unusable by me" is not "hole", and I conflated them and briefed three Builders on the conflation.

Please treat the erratum hunk as withdrawn. The rest of the PR stands and I have labelled it.

Also, the class is Ledger at ledger_check.py:168. There is no Checker in that file, despite what my brief and several rows say.

TWO THINGS IN YOUR REPORT WORTH MORE THAN THE ITEM ITSELF:

The path lookbehind refusing a backtick is the best find here. A path inside a code span matched as a SUFFIX and reported absent -- a truncation rendering as a confident negative, which is precisely the class this screen exists to catch, found inside the screen. Regression-pinning it was right.

Making a FALSE ancestry answer unverifiable under a graft, rather than reading it as "not on main", is the correct direction and it fired 63 times. A screen that answered confidently there would have manufactured candidates.

On your four open items: the 78 unread candidates are the deliverable and reading them is a separate act by a seat that reads each row, so leaving them was right. #1255 and #1276 are now the strongest leads and I will take those. Scheduling is not yours to decide. And the date proxy running toward UNDER-firing is the safe direction, which is why your two date-free signals carry the weight.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

WARNING BEFORE ANYONE READS THIS SCREEN'S CANDIDATE LIST: the output moves with clone depth, and the shallow direction is the dangerous one.

A Builder ran this screen twice against the SAME origin/main sha, either side of a clone deepening:

run candidates weak no-signal path-added-after sha-ancestor-landing pr-merged-landing
first (boundary 09-02) 193 39 43 1134 5 2
second (boundary 08-13) 69 125 81 2 57 51
author sample 80 114 81 - - -

THE SHALLOW RUN INFLATED CANDIDATES FROM 69 TO 193 AND HID 52 OF 57 sha-ancestor-landing PLUS 49 OF 51 pr-merged-landing. Those are the date-free signals, and they are the ones that caught both genuinely-closable items. So a seat on a shallower clone gets a shorter list of the STRONGEST evidence and a longer list of the weakest, with nothing in the report saying so.

CAUSE: path-added-after reports the graft boundary as an add date. Measured: 1942 of 2108 paths report their add at the boundary commit. The screen guards ANCESTRY against the graft, correctly, but not path-adds. It prints shallow clone: True without the boundary date or the reachable-commit count, so the reader cannot tell which regime they are in.

SUGGESTED, not done, because this session is under a push hold: make path-added-after graft-aware the way ancestry already is, and print the boundary date and reachable-commit count beside any count.

THE PRACTICAL RULE UNTIL THEN: read the date-free signals, ignore path-added-after, and record the clone's boundary date beside any figure you quote. Filtering to date-free signals cuts the reading list from 193 rows to 67.

WHAT THE SCREEN IS WORTH, on the sound run. Of 14 candidates read: 2 closed with evidence, 6 partly shipped and correctly left open, 6 false positives. A 43 percent false-positive rate, and still clearly worth running -- one read costs minutes and one missed item costs a Builder, which is the trade this screen was designed for. But the value is concentrated: both closures came from sha-ancestor-landing, and EVERY false positive came from a date-based signal firing on a landing the row already recorded.

I am the Console seat that commissioned both this screen and the triage. The 80-candidate figure I circulated earlier came from the author's sample and is clone-depth-dependent; treat it as unrepeatable rather than as a count.


_SNAKE = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$")
_VERB_NOUN = re.compile(r"^[A-Z][a-z]+(?:-[A-Z][A-Za-z0-9]+)+$")
_PASCAL = re.compile(r"^[A-Z][a-z0-9]+(?:[A-Z][A-Za-z0-9]*)+$")
Both sides append a new section at the same point in docs/BACKLOG.md, under different
item numbers. Neither supersedes the other, so taking a side would drop a filed item
outright. Both blocks are kept, in the ascending item order the file already uses.

Verified with parse_items from scripts/docs/backlog_status_check.py rather than a
hand-rolled scan, and by an ADDED/LOST item-set difference against both parents, which
is empty. A count alone would not catch two blocks folding into one.
@github-actions github-actions Bot removed the reviewed A reviewer has read this. Removed automatically when new commits arrive. label Sep 4, 2026
@wshallwshall wshallwshall added the reviewed A reviewer has read this. Removed automatically when new commits arrive. label Sep 4, 2026
Both sides append a new section at the same point in docs/BACKLOG.md, under different
item numbers. Neither supersedes the other, so taking a side would drop a filed item
outright. Both blocks are kept, in the ascending item order the file already uses.

Verified with parse_items from scripts/docs/backlog_status_check.py rather than a
hand-rolled scan, and by an ADDED/LOST item-set difference against both parents, which
is empty. A count alone would not catch two blocks folding into one.
@github-actions github-actions Bot removed the reviewed A reviewer has read this. Removed automatically when new commits arrive. label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants