Found by a production audit of v0.5.1.
The sequence, which is ordinary
A Limit: password hashing must stay bcrypt cost 12 Record-Id: r-old001
B Limit: password hashing must stay argon2id Record-Id: r-new001 Supersedes: r-old001
B mirrored into refs/notes/commitlore (what post-commit and squash inheritance do)
git reset --hard A (B is now unreachable)
git log says the repository contains exactly one record: r-old001, bcrypt.
context (index) -> r-new001, active, src ["notes"]
context --no-index -> r-new001, active
stale -> 0 superseded, 0 expired, of 1 record in 1 commit
Both failure directions at once. A record from an abandoned commit is served as active, and the record that survives in history is withheld because that abandoned commit retired it.
--no-index reproduces it, so this is the engine, not index staleness.
Cause
src/core/index-db.ts:658-679 — readNoteRecords enumerates git notes --ref=refs/notes/commitlore list with no reachability filter, reads them --no-walk (:683), and resolves paths the same way (:718). src/core/query.ts:585-600 folds them and :804 serves them without asking whether the annotated commit is in HEAD history.
stale disagrees because src/commands/stale.ts:131-134 does filter notes to commits in the walk. Same split as #350: two commands, one repository, different answers.
Window
Until git gc --prune removes the abandoned commit — default two weeks to ninety days. After gc --prune=now the answer correctly reverts, which confirms the mechanism.
No notes.rewriteRef is configured anywhere (grep -r rewriteRef src/ docs/ spec/ README.md → nothing), so a rebase never carries the mirror forward. The phantom is the expected outcome of "record a decision, then rebase before pushing" — which is a normal thing to do.
Fix
A reachability filter on readNoteRecords, matching what src/commands/stale.ts:131-134 already does. The two paths disagreeing is itself the signal that one of them is wrong.
Untested
test/notes.test.ts and test/stale.test.ts:691-812 cover notes/commit merge, divergence and notes-only supersession — never reachability.
Found by a production audit of v0.5.1.
The sequence, which is ordinary
git logsays the repository contains exactly one record:r-old001, bcrypt.Both failure directions at once. A record from an abandoned commit is served as active, and the record that survives in history is withheld because that abandoned commit retired it.
--no-indexreproduces it, so this is the engine, not index staleness.Cause
src/core/index-db.ts:658-679—readNoteRecordsenumeratesgit notes --ref=refs/notes/commitlore listwith no reachability filter, reads them--no-walk(:683), and resolves paths the same way (:718).src/core/query.ts:585-600folds them and:804serves them without asking whether the annotated commit is in HEAD history.staledisagrees becausesrc/commands/stale.ts:131-134does filter notes to commits in the walk. Same split as #350: two commands, one repository, different answers.Window
Until
git gc --pruneremoves the abandoned commit — default two weeks to ninety days. Aftergc --prune=nowthe answer correctly reverts, which confirms the mechanism.No
notes.rewriteRefis configured anywhere (grep -r rewriteRef src/ docs/ spec/ README.md→ nothing), so a rebase never carries the mirror forward. The phantom is the expected outcome of "record a decision, then rebase before pushing" — which is a normal thing to do.Fix
A reachability filter on
readNoteRecords, matching whatsrc/commands/stale.ts:131-134already does. The two paths disagreeing is itself the signal that one of them is wrong.Untested
test/notes.test.tsandtest/stale.test.ts:691-812cover notes/commit merge, divergence and notes-only supersession — never reachability.