Closes#1.
Findings are keyed on (fingerprint, path, detail, byte_offset), so editing a
file moves every offset after the edit and the next sweep inserts new rows
beside the old ones. Nothing removed them, and the count at the top of
`nanny status` drifted away from reality.
The obvious fix — retire anything the last sweep did not re-confirm — is
wrong, and wrong in the direction that matters. Tail sources resume from a
cursor, so a finding at offset 500 of a 40 MB transcript is never
re-confirmed on a normal sweep; whole-mode sources are stat-skipped entirely
when unchanged. Both are indistinguishable from "the secret is gone" if you
look only at the findings table, and reaping on absence would have retired
every real transcript spill on the machine.
So collectors make a positive claim instead. ExcerptSink::note_complete_scan
declares that a container was read cover to cover — whole-mode files that
were actually opened, tail files whose cursor was invalidated by rotation,
truncation or --full, the database only from an empty cursor. It defaults to
a no-op so a collector cannot make the guarantee by accident: silence costs
a stale row, a wrong claim costs an exposure reported as handled.
Two outcomes follow. Superseded — the same secret still in the same file at
a new offset — merges into its successor and the stale row goes, carrying
first_seen and the operator's triage where the successor has none. That is
the real win: a decision now survives a reformat, and triage that evaporates
when someone runs a formatter is triage nobody does twice. Vanished — gone
from the container, or the container gone — is recorded and explicitly not
resolved. A spill edited away is not a spill that never happened; the value
already reached a model provider, and the row is the only thing that still
says a rotation may be owed. Hence a vanished_at timestamp orthogonal to
status rather than a status of its own, findings shown as "(gone)" rather
than hidden, and re-confirmation clearing the mark so a briefly unavailable
filesystem heals itself.
Six tests, of which the two that matter most assert the negative: an
incremental tail and a stat-skipped source must retire nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAqHWfdMAsYu1o36tgeima