test: arm orphan-scan in the matrix runner (#983, #1015) - #1070
Conversation
The gate answers "has this run a check the ledger has never seen". Nothing answered the other half -- "does the ledger name a check that no longer exists" -- and two rows naming deleted checks sat in the committed ledger from #917 until `orphan-scan` was written for exactly that defect and HAD NO CALLER IN THE TREE. Its only invocations were the selftest arms testing the tool itself: tested, and unable to fire on anybody's change. ONE LOG PER CALL, NOT ALL OF THEM. `_by_run` returns one entry per LOG, so `len(runs) > 1` is true whenever more than one file is passed even when every log came from the same matrix run. A call shaped like the gate's `$_led_logs` is refused by the COUNT, before the before-log/after-log union the error names. So the runner loops and reduces the statuses. --orphans-only, BECAUSE A SKIP IS NOT A DELETION. By default rc=1 also covers a part that skipped, and that default is correct on the merits: `not checked` is silence by construction and OUT of scope, `unprunable` is a gap in a part the run CLAIMED and in it. Three arms in part 410 pin that and their names argue for it. So this does not change what rc=1 means. I was going to -- `return 1 if orphans else 0` -- and reading 410 stopped it: that deletes a deliberate contract to make the wiring convenient, which is weakening a guard to get green. The flag changes the QUESTION instead. With it rc=1 is orphans and nothing else; without it the old question and the old answer. rc=1 never acquires a second meaning, and `unprunable` is printed either way, so narrowing what the gate refuses on cannot hide it. MEASURED BEFORE WIRING, and the clean run proved less than it looked: only part 340 of 46 has a real check_skip; 320/400/410/480 match on comments, a grep-based sweep and printf fixtures -- source-derived, box-independent none of CI's 9 skipped suites has a ledger row, so none can put a part in scope five majors on one box: orphans=0 unprunable=0 rc=0 BUT part 340 recorded 108 PASS and 0 SKIP, so the skip path never fired The skipped-part case was therefore forced synthetically rather than waited for, and the tool comes out well: the siblings are classified `unprunable`, not orphans -- "absence there is not removal". REMOVAL PROOF, both directions: a planted row naming a deleted check rc=1, orphans=1 refused a part that skipped rc=0 not refused unprunable still reported harness_selftest 962 passed + 0 failed + 0 unrunnable + 0 skipped shellcheck -S error test/run_all_versions.sh clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
351f73f to
bb6dfe4
Compare
|
Self-review found a defect in this PR and I have force-pushed the fix: It was unintentionally stacked on #1062. I branched from that branch rather than from A reviewer reading it would have been shown #1033's ledger rows and part-400 premises as if they belonged to this change, and the PR body claims only #983/#1015. That is the "review the file list, not just the fix" rule, and I broke it on my own work. Rebased onto The row count differs from the earlier run (960 vs 962) because this branch no longer carries #1062's two part-400 arms — which is the point. |
|
Adversarial review. The design is right and the I checked one thing first and was wrong, so it is worth recording: the CI log shows 242 of 246 invocations reporting 242 zero-row calls are the uncovered suites, correctly silent. The wiring works. 1. The red is self-inflicted, and it is the class this PR is about
Both 2.
|
, #1015) CI refused this branch for exactly the reason I had spent the hour posting recipes about on four other PRs: not in the ledger: harness_selftest 410-a-check-must-have-been-red a skipped part is NOT an orphan, so --orphans-only returns 0 (on major 17) ... and five more The six arms this PR adds are checks the committed ledger has never seen, and the gate refuses a check it has never seen on the major being run. I reviewed that defect on #1039, #1063, #1065 and #1068 and then shipped it myself. TWO THINGS WORTH RECORDING FROM THAT. `harness_selftest.sh` GREEN DOES NOT COVER THE LEDGER GATE. The gate runs in run_all_versions.sh, not in the suite, so a local suite run passes while the matrix refuses. Every local verification I did on this branch was of the suite. AND THE WIRING WORKS, which the same failing log shows: orphan scan: parts in the run=1, rows in those parts=0, orphans=0, unprunable=0, not checked=1217 That is this PR's own change running in CI for the first time. Fixed the way 3a640b0 established: harness_selftest run on all five majors and all five logs merged, so the majors field is OBSERVED rather than written. PG15..PG19 rc=0 960 passed + 0 failed + 0 unrunnable + 0 skipped, each merge: rows=1223 | runs=5, distinct checks this merge=960 majors: uniform, all 1223 rows carry 15;16;17;18;19 rows 1223 = sum of buckets printed 1223 ledger 1217 -> 1223, purely additive: 0 removed, 6 added CENSUS RE-DERIVED BY COUNTING, not by adding 6 to 1209: awk -F'\t' '$5=="never"' test/check_ledger.tsv | wc -l -> 1215 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
…#983, #1015) @OffgridwithJD found two defects in the runner wiring, neither of which any arm in this PR could see, because every proof I had written was of the TOOL. THE REDUCTION OVERWROTE. `|| _orph_fail=$?` across N logs tells the operator about whichever log failed LAST. Measured with a stub, both orders: rcs=[1 2] reports "could not run the orphan scan" and hides a real orphan rcs=[2 1] reports "names a check that no longer exists" and hides the broken tool The verdict was right both times -- verfail=1 either way -- and the DIAGNOSIS was wrong half the time. That is the defect the gate's own comment thirty lines above says it fixed, in its own words about sending the reader at the wrong repair, which is how I know a comment does not transfer to the code beneath it. AND THE OBVIOUS REPAIR IS WORSE THAN THE BUG, which they measured before suggesting it: `|| { [ "$?" -gt "$_orph_fail" ] && _orph_fail=$?; }` yields 0 for EVERY input, because `$?` inside the braces is the `[` test. It turns an order-dependent misdiagnosis into a silent pass. Two independent conditions need two independent flags, not a max: keeping the worst still hides a real orphan behind a tool failure. Both messages can now print, and the second says that both can be true. THE WIRING IS NOW PROVED, not just the tool. The arms EVAL THE RUNNER'S OWN TEXT, the way link 5 of part 330 does, because an arm that re-derives the rule tests the world instead of the code: premise: the runner's orphan reduction was extracted, not an empty range both conditions survive the reduction whatever order they arrive in (1 2) both conditions survive the reduction whatever order they arrive in (2 1) control: all-clean logs leave both flags down control: one orphan among clean logs raises only the orphan flag REMOVAL PROOF ON THE WIRING: collapsing the two flags back into one reddens both order arms with `got [10] want [11]` while the control stays green. harness_selftest 965 passed + 0 failed + 0 unrunnable + 0 skipped shellcheck -S error test/run_all_versions.sh clean PG15..PG19 rc=0, 965 each; merge rows=1228, majors uniform at 15;16;17;18;19 census re-derived by counting: 1220 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
linuxhikerpm
left a comment
There was a problem hiding this comment.
The other half of the ledger comparison is now actually called. orphan-scan sits after the per-major gate (not inside pgc_tally_suite), one log per invocation because _by_run is per log, and --orphans-only keeps a box-dependent skip from sharing rc=1 with a deleted check. Two flags instead of || _orph_fail=$? is the right reduction: I read the 410 arms that eval the runner's own case and they pin both orders plus the two controls.
Eleven new never rows, census 1209→1220, majors observed on 15–19 rather than written. CI 14/14 green, including both suite legs.
Landing: CHANGELOG also inserts a one-line #1033 heading that belongs to #1062. Drop that line so the two PRs do not collide; this branch does not contain the piped-loop premises. Not merging.
OffgridwithJD
left a comment
There was a problem hiding this comment.
APPROVE at 57f5c67. Both findings are fixed, and I verified the one link your new arms still do not cover rather than asking you to add another.
The reduction fix is right
Two independent flags, _orph_rc=$? captured immediately after the command with nothing between, case covering 0, 1 and *, both flags reset per major inside the block that runs per major. No single-variable reduction, so neither answer can eat the other.
The wiring arms are the right shape
Evaluating the runner's own case text rather than a copy is what makes them arms about this code instead of about a restatement of it. And the premise —
premise: the runner's orphan reduction was extracted, not an empty range -> grep -c '_orph_orphan=1' == 1
— is the part that matters: eval "" is a no-op and every one of the four arms below it would pass on an empty extraction. The awk pattern is tab- and column-exact, so a reindent breaks extraction rather than silently testing nothing, and the premise catches that. Fails closed.
Two controls, both correct: all-clean leaves 00, one orphan among clean logs raises 10 and not 11.
What the arms still do not reach, and what I did about it
They prove the flags. Nothing in them touches verfail, so the step from flag to red build was still unproven — which is the exact shape of the original defect, one link further along. Rather than ask for a sixth arm I extracted the two if blocks from your file and drove them the same way you drive the case:
orphan=0 broken=0 -> verfail=0 "not a pass" lines=0
orphan=1 broken=0 -> verfail=1 "not a pass" lines=1
orphan=0 broken=1 -> verfail=1 "not a pass" lines=1
orphan=1 broken=1 -> verfail=1 "not a pass" lines=2 <- both print, as the comment claims
and confirmed verfail is the verdict at :1567 — non-zero gives FAIL PG$major and sets overall=1.
So the whole chain is now verified end to end:
orphan in a log -> rc=1 -> _orph_orphan=1 -> verfail=1 -> FAIL PG17 -> overall=1
That took me two attempts and the first was wrong in a way worth recording: I ran the extracted block inside out="$(eval ...)", which is a subshell, so verfail=1 was set and discarded and I measured verfail=0 on every input. Had I stopped there I would have filed a defect that does not exist. An assignment measured through a command substitution is measured in a process that is about to exit.
If you want the arm anyway it is four lines beside the ones you have, and it would close the class rather than the instance. Not a condition of this approval.
The ledger is clean
410-a-check-must-have-been-red 196 rows, all 15;16;17;18;19
whole file 1228 rows, uniform
census 1220 == budget 1220
CI 14/14, 0 failures, 0 pending
Which is also the fifth PR to have hit the single-major trap — I filed #1071 on it, because the merge summary already computes the distribution it fails to warn against, and the recipe the gate prints says <log> singular.
What I would keep from this PR beyond the code
Your note that harness_selftest.sh green does not cover the ledger gate, because the gate runs in run_all_versions.sh. That is the same shape as the pytest exit-67 finding: a local verification that cannot reach the thing CI checks, so the author sees green and the gate sees red. Both belong written down somewhere an author looks before pushing, not only in two PR bodies.
Closes #983 and #1015 properly: the tool now has a caller, and the caller has been shown to fail.
THE COLLISION I FLAGGED ON FOUR OTHER PRs, ON MY OWN. Both sides carried a
`checks_never_observed_red` correct for the tree that derived it:
this branch 1211
#1070 1220
merged truth 1222
Neither survives. Re-derived by COUNTING, which is the only resolution a census
has -- adding the deltas gives a number no tree collects, and arithmetic is only
accidentally right when rows are added rather than a verdict changing:
awk -F'\t' '$5=="never"' test/check_ledger.tsv | wc -l -> 1222
AND check_ledger.tsv AUTO-MERGED SILENTLY, which is the half that does not ask.
Verified rather than trusted: 1230 rows, majors uniform at 15;16;17;18;19, this
branch's two part-400 rows and #1070's part-410 rows all present.
harness_selftest 967 passed + 0 failed
test_the_committed_ledger_and_budget_agree 1 passed, 7 checks
orphan-scan on this tree orphans=0, unprunable=0, rc=0
ONE NEAR-MISS WORTH RECORDING. The first orphan-scan run on the merged tree
reported `orphans=2`, and they were this branch's own two new checks. The log was
from #1070's tree, taken before these arms existed, so the rows had no matching
records -- a STALE LOG, not a deleted check. Re-run against a log from this tree it
is 0. A ledger comparison is only meaningful against a log of the same tree, and
the failure mode reads exactly like the defect the tool exists to find.
CHANGELOG: both entries kept; #996's single anchor again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
CHANGELOG only, and the two entries are independent -- kept both. Counts re-derived BY COLLECTION on the merged tree rather than assumed unchanged, because commandprompt#1070 touched test/ and a count that happens to still be right is not the same as a count that was checked: guard 342 collected (tracked 342) cluster 373 collected (tracked 373) Both legs on the merged tree, pg16a: guard leg 342 collected, 879 checks, 0 fail rc=0 cluster leg 373 collected, 1025 checks, 0 fail rc=0 This branch adds no bash checks, so commandprompt#1070's newly-armed orphan-scan has nothing to say about it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
No conflicts. Counts re-derived BY COLLECTION on the merged tree:
guard 342 collected (tracked 342)
cluster 406 collected (tracked 406)
Both legs, pg16a:
guard leg 342 collected, 880 checks, 0 fail rc=0
cluster leg 406 collected, 1100 checks, 0 fail rc=0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
The only conflict is CHANGELOG.md. Both sides add to `### Added`, so both entries are kept. The resolution also deletes one line that neither side should keep. `bb6dfe4` (#1070, cherry-picked off this branch) carried this change's CHANGELOG HEADLINE onto main without its body, so main currently holds: - The piped-loop sweep reported a clean tree without reading one (#1033). - Userinfo in an object-store ENDPOINT was accepted, ... a heading with no entry under it. This branch had both that orphan and the full entry, from an earlier merge of main. The merged file now carries the entry once, with its body, and no bodiless heading anywhere in `[Unreleased]`: occurrences of the headline 1 line 21 has body bodiless headings in [Unreleased] 0 Nothing else moved. Per-file patch md5 against the approved head 25a6350, added and removed lines only: test/check_ledger.tsv 71def1f929ca same test/check_ledger_budget.txt 0262d67f19b4 same test/selftest/400-...-machine.sh b3c5f1bf132e same Ledger re-derived from the merged files rather than read off the diff: main 2f288d3 1228 rows never 1220 budget 1220 this branch 1230 rows never 1222 budget 1222 MERGED 1230 rows never 1222 budget 1222 consistent majors uniform 15;16;17;18;19 on all three, suites_not_covered held at 249. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
A contributor adds checks, runs the suite on ONE major, merges that log. The row lands with `majors = 18`. The gate considers a row only where its majors intersect the run's, so `suites (PG 18)` matches it and is green while `suites (PG 17)` reads it as a check the ledger has never seen and reddens -- naming the contributor's own checks `(on major 17)`, which reads as though their suite is broken on 17 when it passes there. FIVE AUTHORS IN A ROW hit it, including the person who wrote the tool, on a PR that was itself about ledger hygiene: #1039, #1063, #1065, #1068, #1070. When everyone makes the same mistake it is the tool's shape, not five lapses. And the tool already knew: the distribution it prints for its summary line is computed from the same rows. `merge` now warns, naming the rows, the set they carry, the set the rest of the ledger carries, and the majors the gate will redden on. Four decisions, each with an arm. STRICT SUBSET rather than inequality, so a row naming a major the ledger has never carried -- how a new major legitimately enters -- is not warned about. ONLY ROWS THIS MERGE TOUCHED, or a partly-seeded ledger reprints its own history every time. NOTHING TO COMPARE AGAINST IS NOT A WARNING, so seeding an empty ledger stays quiet. REPORTING RATHER THAN A REFUSAL, because seeding one major at a time is how a contributor without five installed majors makes progress; the gate still refuses later, this only makes that refusal predictable at the moment it is caused. The prevailing set is the PLURALITY among untouched rows, not a union: a union cannot represent a minority set, which is the defect #1048 fixed in the summary line one level up. And the recipe that produced it. The gate printed `--date <today> <log>`, singular, so following it exactly writes the broken row. It now names one log per gated major and says why. Both harnesses, independent: selftest part 520 and six arms in test/pytest/test_mutation_ledger.py, neither naming the other. Removal proof, four mutations, each mutant asserted to parse: strict subset -> inequality the new-major arm reddens sweep all rows, not touched the pre-existing-minority arm reddens warning removed entirely four arms redden recipe reverts to one log the recipe arm reddens control 992 passed + 0 failed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
A contributor adds checks, runs the suite on ONE major, merges that log. The row lands with `majors = 18`. The gate considers a row only where its majors intersect the run's, so `suites (PG 18)` matches it and is green while `suites (PG 17)` reads it as a check the ledger has never seen and reddens -- naming the contributor's own checks `(on major 17)`, which reads as though their suite is broken on 17 when it passes there. FIVE AUTHORS IN A ROW hit it, including the person who wrote the tool, on a PR that was itself about ledger hygiene: #1039, #1063, #1065, #1068, #1070. When everyone makes the same mistake it is the tool's shape, not five lapses. And the tool already knew: the distribution it prints for its summary line is computed from the same rows. `merge` now warns, naming the rows, the set they carry, the set the rest of the ledger carries, and the majors the gate will redden on. Four decisions, each with an arm. STRICT SUBSET rather than inequality, so a row naming a major the ledger has never carried -- how a new major legitimately enters -- is not warned about. ONLY ROWS THIS MERGE TOUCHED, or a partly-seeded ledger reprints its own history every time. NOTHING TO COMPARE AGAINST IS NOT A WARNING, so seeding an empty ledger stays quiet. REPORTING RATHER THAN A REFUSAL, because seeding one major at a time is how a contributor without five installed majors makes progress; the gate still refuses later, this only makes that refusal predictable at the moment it is caused. The prevailing set is the PLURALITY among untouched rows, not a union: a union cannot represent a minority set, which is the defect #1048 fixed in the summary line one level up. And the recipe that produced it. The gate printed `--date <today> <log>`, singular, so following it exactly writes the broken row. It now names one log per gated major and says why. Both harnesses, independent: selftest part 520 and six arms in test/pytest/test_mutation_ledger.py, neither naming the other. Removal proof, four mutations, each mutant asserted to parse: strict subset -> inequality the new-major arm reddens sweep all rows, not touched the pre-existing-minority arm reddens warning removed entirely four arms redden recipe reverts to one log the recipe arm reddens control 992 passed + 0 failed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
Closes #983 and #1015. They are one piece of work: #983 asks for a guard that set-compares the ledger's keys against a run's, and that guard is
orphan-scan, which #1015 reports has no caller.#983's other half — the two rows naming deleted checks — was already removed by
dfc0892. I checked the names against the whole tree rather than the issue's list: zero hits outside the ledger, zero inside it now.The defect
The gate answers "has this run a check the ledger has never seen". Nothing answered the other half, and two rows naming deleted checks sat in the committed ledger from #917 until #983 found them while doing something else. The census counted both.
orphan-scanwas written for exactly that and had no caller in the tree — its only invocations were the selftest arms testing the tool itself. Tested, and unable to fire on anybody's change.Two things the wiring had to respect
One log per call.
_by_runreturns one entry per log, solen(runs) > 1is true whenever more than one file is passed, even when every log came from the same matrix run. A call shaped like the gate's$_led_logsis refused by the count, before the before-log/after-log union the error message names. So the runner loops.--orphans-only, because a skip is not a deletion. I was going to changereturn 1 if (orphans or unprunable)toreturn 1 if orphans, reasoning thatunprunableis "cannot speak" exactly asnot checkedis.Reading
selftest/410stopped that. Three arms pin the current behaviour and their names argue for it:The equivalence I was relying on is false, and it is why the third bucket exists:
not checkedis silence by construction and out of scope;unprunableis a gap in a part the run claimed and in it. So the default is right on the merits, not merely by precedent.The flag therefore changes the question, not the severity of an answer. With it
rc=1is orphans and nothing else; without it, the old question and the old answer.rc=1never acquires a second meaning — which is the same defect one level up — andunprunableis printed either way, so narrowing what the gate refuses on cannot hide it.Measured before wiring, and the clean run proved less than it looked
That last line is the point: the skip path never fired, so five clean majors tested nothing about skips. The case was forced synthetically instead, and the tool comes out well — siblings of a skipped arm are classified
unprunable, not orphans. The docstring's own horror story (--prunedeleting a suite while reportingnot checked=0andrc=0) is a bug that classification already fixes.Removal proof, both directions
The three arms pinning the default contract still pass, which is what shows the flag narrowed the question rather than weakening them.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK