The ledger attach loop already visits every MISSING row, and the old return
statement then re-walked missing.values() three more times to recover matched,
new and tolerated. Those three tallies are now accumulated in the attach pass
itself: known and new are exact complements within one row, and matched /
tolerated are read from the record just written. Four traversals of the row
set become one.
Output is unchanged, and that is measured rather than asserted. Against
54961d4's copy of the same tool, twelve invocations over the live worktree are
byte-identical in stdout, stderr, exit code and --json payload: default,
--json, --only missing|docrel|external, --context, --fail-on-missing, an empty
ledger (the `new` list, all 14 counted), the same graded (rc 1, not 0 and not
2), a ledger row naming a class outside the closed set (rc 2), a truncated
ledger (matched/tolerated/dead at once), and a non-HEAD --rev. Selftest is
31/31 on both copies.
No speed claim: five timed runs put HEAD at 6.94/6.92s median and this at
6.70/6.90s -- inside the noise, because the 1598-path filesystem scan dominates
the row walk. The change buys passes, not milliseconds.