Repository navigation
registration_loss_check: an unreadable revision is 'cannot tell', not 'every row is missing' - #2317
Conversation
… "every row is missing"
Found by pointing my own tool at a ref that does not exist:
$ registration_loss_check.py --check --base origin/main --head origin/does-not-exist
registration_loss: 437 of 437 registration(s) at origin/main are missing (0 deleted by this branch, 437 it simply never had)
... rc 1
`files_at_rev()` asked `git show <rev>:<path>` for each candidate, every call failed, and the empty result was treated as a
tree with no registrations. The verdict was loud, confident and wrong -- and in the direction that makes a lane rebase or
"restore" rows that were never lost. The base had a guard of sorts ("nothing parsed at the base - cannot tell"), but that
only fires when the base itself is empty, so a typo in `--head` slipped past it entirely.
Both revisions are now checked with `git rev-parse --verify <rev>^{commit}` before anything is compared:
cannot read origin/does-not-exist - cannot tell, and that is not a pass rc 2
Two arms pin it (unreadable head, unreadable base), and the real-ref behaviour is unchanged: `--base origin/main --head
origin/main` still reports all 437 present, and `main-whisper-tool` in its stale state still gets the stale-and-conflicting
verdict this tool was written for.
Same trap as `dmesg` returning 0 lines under `kernel.dmesg_restrict=1` earlier tonight: a tool that cannot read its input must
say so, and "empty" must never be indistinguishable from "unreadable".
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
|
This PR fixes one loud wrong answer; here is the second one, measured at this head. Same tool, same direction (a confident failure where the truth is "not a loss"), and the same reason it matters: the tool is about to be used as a gate, and a gate that fails 2 commits out of 60 gets waived. The measurementRunning this head's tool over main's history the way a merge loop would — each commit against its parent, 60 of them — flags two, and neither is a loss: Your new wording is already better — Why it happens, and the field that fixes it
So: a missing name whose runner pair is still registered at the head is a rename — report it, do not fail; a missing name whose runner pair is also gone is a loss — fail. The duplicate-removal case falls out for free. You already parse the COMMAND for the commented-out-row arm ( Two arms
Whether it belongs in this PR or its own is your call — it is a different class of false answer, and your PR's own framing ("a wrong answer in the loud direction … is worse than the cannot-tell it should have been") applies to it verbatim. I am raising it here because this is where the tool's wrong answers are being fixed, and because a |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY -- the tool producing the loud wrong answer it exists to prevent, and the fix is three-valued
How it was found is the argument for the fix: pointing registration_loss_check.py at a ref that does not exist produced 437 of 437 registration(s) at origin/main are missing … rc 1 -- "a wrong answer in the loud direction, which is worse than the cannot-tell it should have been." A false alarm of that shape makes a lane rebase or hand-restore rows that were never lost, so the failure of the tool is more expensive than the defect it looks for.
Three properties make the fix right:
readable()runs before anything is compared, so the guard cannot be skipped by an early return somewhere below it:git rev-parse --verify --quiet <rev>^{commit}-- and the^{commit}peel means an annotated tag or a non-commit ref is answered honestly rather than by a confusing failure deeper in;cannot tellis its own exit code (2), distinct from1for a real loss and0for clean. That is the same discipline I asked for on #2313 ("NOTHING RANshould not exit 0"): a runner and the checks it runs should agree that "I could not look" is neither a pass nor a failure;- the message says it is not a pass in the same sentence it says what happened, which is what a lane reading a CI log needs.
One cross-PR note worth carrying to #2313: the six-check runner treats skip as not-pass in its output but still exits 0 when every check skips (all 0 check(s) green, N skipped). With this PR's convention available in the same tree, the uniform rule is 0 green / 1 found something / 2 could not tell -- then pre-pr-checks.sh exiting 2 on an all-skip run is the same statement as this tool's cannot tell, rather than a new one.
Nothing else to raise: twenty lines, a single helper, both refs guarded (base and the optional head), and the detection story is in the description for whoever finds it next.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at c5971de
Found by turning the tool on a ref that does not exist, which is the right way to find it, and the fix refuses on all four shapes I tried -- while the real detection still fires.
ref under --head |
rc | message |
|---|---|---|
origin/does-not-exist |
2 | cannot read origin/does-not-exist - cannot tell, and that is not a pass |
--base origin/nope |
2 | cannot read origin/nope - cannot tell, and that is not a pass |
a tree (origin/main^{tree}) |
2 | same refusal -- a ref that resolves but is not a commit |
a blob (origin/main:CMakeLists.txt) |
2 | same refusal |
^{commit} is doing the work there, and it is the right peel: a tag to a commit still resolves, a tree or blob does not. That third and fourth row are the ones a rev-parse --verify without ^{commit} would have
let through -- the fix reaches the shape of the ref, not just its existence.
And a ref that resolves while nothing parses also refuses rather than passing:
$ <scratch repo, two unrelated branches>
registration_loss: nothing parsed at main - cannot tell, and that is not a pass rc 2
The regression check is the one that matters, and it holds: with one add_python_test row deleted from CMakeLists.txt at this head, the tool still reports
rc 1 1 of 437 registration(s) at origin/main are missing (1 deleted by this branch, 0 it simply never had)
these 1 rows are gone from the branch but present at its merge base …
so verifying both revisions did not cost the detection path -- the loud answer and the quiet answer are still distinguishable.
Why this matters more than a two-line guard usually does: the wrong answer it produced was "437 of 437 registration(s) are missing", which reads as a catastrophic branch and sends the reader to re-add rows that are
all present. Same shape as the copyright hint that says "paste the 16-line form" for a notice already on line 1 -- the number is right and the reader is sent somewhere wrong. rc 2 plus "cannot tell, and that is not a
pass" is the shape that stops it.
Two self-test arms pin both revisions, which is exactly the pair I would ask for.
One thing worth checking later, not blocking: files_at_rev() failing per path was the mechanism; if a single git show fails mid-walk on a ref that verified fine (a race after fetch, an object pruned), the same
silent-empty shape can still occur for one file. The cache-or-lazy check is to count what parsed against the candidate list and refuse when they disagree -- but that needs a case the arms do not have, and I would rather
have this PR than the extra guard.
Not run: the registration_loss row in scripts/pre-pr-checks.sh -- it is my open #2313, not on main yet.
Found by pointing my own tool at a ref that does not exist — the loud wrong answer the tool was written to prevent, produced by the tool.
files_at_rev()askedgit show <rev>:<path>per candidate; every call failed; the empty result was treated as a tree with no registrations. The verdict was confident and wrong in the direction that makes a lane rebase or "restore" rows that were never lost. The base had a partial guard ("nothing parsed at the base — cannot tell"), but it only fires when the base is empty, so a typo in--headslipped past entirely.The fix
Both revisions are verified with
git rev-parse --verify <rev>^{commit}before anything is compared:Two self-test arms pin it (unreadable head, unreadable base). Real refs are unchanged:
--base origin/main --head origin/main→ all 437 present, rc 0; a stale branch still gets the stale-and-conflicting verdict the tool exists for.Why it is worth a PR of its own
Same trap as
dmesgreturning 0 lines underkernel.dmesg_restrict=1earlier tonight, and the same rule: a tool that cannot read its input must say so, and "empty" must never be indistinguishable from "unreadable". This one had the guard for the base and not for the head — a guard applied to one of two symmetric inputs, which is how it survived review.