You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Extracted from #79's pathology list ("b2id collision across notes") so #79 can close on its PR's merge. This is the one candidate self-healing behavior from that list that #79's implementation deliberately did not cover, because it can't be diagnosed from a single file's parse — it needs index-wide knowledge, i.e. the reindex/projection pass.
The gap
The in-app path can no longer create a collision — Vault::write_frontmatter (#79) refuses any edit that changes, removes, or duplicates the open note's b2id. But the external paths #79 is really hardening against still can:
Duplicate-file collision. Copy a note in Finder (note.md → note copy.md): two files now carry one b2id. Today's behavior (db::upsert_note): ON CONFLICT(b2id) DO UPDATE SET path = excluded.path — the last-projected file silently wins the identity, and the other file is shadowed out of the index entirely (no row, no tree entry, no search hits). Which file wins depends on walk order. Nothing tells the human.
Blanked-id identity churn. An external edit that blanks the value (b2id: with nothing after it) reads as absent (scan_b2id skips empty values — the Invalid YAML frontmatter triggers a b2id re-stamp loop (file grows every reindex; desktop can loop on it) #75 discipline), so the next projection stamps a fresh id: the note's identity changes, every inbound edge keyed to the old id dangles (G5 surfaces them as broken links, correctly), but nothing says why — "this note's identity was restamped" is invisible.
Both are tolerate-don't-corrupt today (nothing crashes, bytes are preserved, W4 is honored) — the missing piece is surfacing, per #79's own framing: "Surface as a fixable notice, not a crash and not a silent rewrite."
Proposed shape
Follow the ProjectReport.skipped precedent (the unreadable-file surfacing): the projection pass already visits every note, so it can cheaply report
collisions: paths that presented an already-claimed b2id this pass ("notes/a.md and notes/a copy.md both claim 01ABC… — the index kept <winner>; give one of them a fresh identity by removing its b2id: line"), and
(possibly) restamped: notes that received a fresh b2id while inbound edges still reference an id no note carries — the churn signature.
Adapters render these as notices (CLI reindex summary; desktop toast/flash, maybe a per-note marker), never auto-fix (W4 — the human decides which file keeps the identity).
Non-goals
No auto-resolution, no tie-breaking heuristics, no writes. Surfacing only.
Extracted from #79's pathology list ("
b2idcollision across notes") so #79 can close on its PR's merge. This is the one candidate self-healing behavior from that list that #79's implementation deliberately did not cover, because it can't be diagnosed from a single file's parse — it needs index-wide knowledge, i.e. the reindex/projection pass.The gap
The in-app path can no longer create a collision —
Vault::write_frontmatter(#79) refuses any edit that changes, removes, or duplicates the open note'sb2id. But the external paths #79 is really hardening against still can:note.md→note copy.md): two files now carry oneb2id. Today's behavior (db::upsert_note):ON CONFLICT(b2id) DO UPDATE SET path = excluded.path— the last-projected file silently wins the identity, and the other file is shadowed out of the index entirely (no row, no tree entry, no search hits). Which file wins depends on walk order. Nothing tells the human.b2id:with nothing after it) reads as absent (scan_b2idskips empty values — the Invalid YAML frontmatter triggers a b2id re-stamp loop (file grows every reindex; desktop can loop on it) #75 discipline), so the next projection stamps a fresh id: the note's identity changes, every inbound edge keyed to the old id dangles (G5 surfaces them as broken links, correctly), but nothing says why — "this note's identity was restamped" is invisible.Both are tolerate-don't-corrupt today (nothing crashes, bytes are preserved, W4 is honored) — the missing piece is surfacing, per #79's own framing: "Surface as a fixable notice, not a crash and not a silent rewrite."
Proposed shape
Follow the
ProjectReport.skippedprecedent (the unreadable-file surfacing): the projection pass already visits every note, so it can cheaply reportcollisions: paths that presented an already-claimedb2idthis pass ("notes/a.mdandnotes/a copy.mdboth claim01ABC…— the index kept<winner>; give one of them a fresh identity by removing itsb2id:line"), andrestamped: notes that received a freshb2idwhile inbound edges still reference an id no note carries — the churn signature.Adapters render these as notices (CLI reindex summary; desktop toast/flash, maybe a per-note marker), never auto-fix (W4 — the human decides which file keeps the identity).
Non-goals
Sources
crates/b2-core/src/db.rsupsert_note(theON CONFLICT(b2id)last-wins),crates/b2-core/src/note.rsscan_b2id,crates/b2-core/src/ingest.rs(projection pass,ProjectReport.skippedprecedent).