Found while property-testing #18 (PR #74) — probing the edge the generators deliberately avoid.
Symptom
A note whose frontmatter fails to YAML-parse gets a fresh b2id: line inserted on every reindex. The file grows without bound, collects one dead ULID per pass, and B2 repeatedly writes a note it was only ever allowed to touch once.
Repro
cat > vault/bad.md <<'MD'
---
: :
title: [unclosed
---
A body line.
MD
b2 reindex && b2 reindex && b2 reindex
# bad.md now carries THREE b2id lines (76 → 109 → 142 bytes in the probe run)
Root cause
note::extract_fields is deliberately best-effort: unparseable frontmatter still round-trips, the fields just come back empty — including b2id: None. Ingest gates stamping on fields.b2id, so an unreadable-but-present id (or a just-stamped one inside still-invalid YAML) reads as "missing" every single pass:
stamp inserts b2id: <new ulid> → surrounding YAML is still invalid → next parse still yields b2id: None → stamp again.
Two invariants break: "the only write B2 makes of its own accord is stamping a missing b2id" (it stamps N of them), and reindex idempotency (full ≡ incremental can't hold when every pass rewrites the file — the note's identity churns ULIDs, so even the index never stabilizes).
Severity — elevated by #73
Before #73 this needed manual reindexes. Since project-on-pulse (#73), the desktop reconciles vault-changed by projecting — and a stamp is itself a vault change: stamp → pulse → project → YAML still invalid → stamp again → pulse… an unbounded write loop (~1 write per debounce window) for any vault containing one bad-YAML note. One hand-edited unclosed bracket in frontmatter is enough to trigger it.
Fix directions (needs a decision)
Stamping is B2's one autonomous write — it should be gated on "definitively absent", never on "couldn't parse":
- (a) Conservative raw scan fallback: when the YAML parse fails, line-scan the frontmatter text for a
b2id: key before stamping (and use its value for projection). Keeps the note indexed and its identity stable; the malformed YAML remains the user's to fix.
- (b) Never stamp unparseable frontmatter: treat a note whose frontmatter exists but won't parse as unstampable — skip the write, surface it like the existing
skipped unreadable-file channel. Safest write posture, but the note drops out of (or churns in) the index.
(a) preserves the most value and matches the byte-honest philosophy; either way the fix should land with a props.rs property over arbitrary/invalid frontmatter (stamp-at-most-once; second reindex writes nothing) and the invalid-YAML case added to the #18 generators.
Refs
Found while property-testing #18 (PR #74) — probing the edge the generators deliberately avoid.
Symptom
A note whose frontmatter fails to YAML-parse gets a fresh
b2id:line inserted on every reindex. The file grows without bound, collects one dead ULID per pass, and B2 repeatedly writes a note it was only ever allowed to touch once.Repro
Root cause
note::extract_fieldsis deliberately best-effort: unparseable frontmatter still round-trips, the fields just come back empty — includingb2id: None. Ingest gates stamping onfields.b2id, so an unreadable-but-present id (or a just-stamped one inside still-invalid YAML) reads as "missing" every single pass:stamp inserts
b2id: <new ulid>→ surrounding YAML is still invalid → next parse still yieldsb2id: None→ stamp again.Two invariants break: "the only write B2 makes of its own accord is stamping a missing b2id" (it stamps N of them), and reindex idempotency (
full ≡ incrementalcan't hold when every pass rewrites the file — the note's identity churns ULIDs, so even the index never stabilizes).Severity — elevated by #73
Before #73 this needed manual reindexes. Since project-on-pulse (#73), the desktop reconciles
vault-changedby projecting — and a stamp is itself a vault change: stamp → pulse → project → YAML still invalid → stamp again → pulse… an unbounded write loop (~1 write per debounce window) for any vault containing one bad-YAML note. One hand-edited unclosed bracket in frontmatter is enough to trigger it.Fix directions (needs a decision)
Stamping is B2's one autonomous write — it should be gated on "definitively absent", never on "couldn't parse":
b2id:key before stamping (and use its value for projection). Keeps the note indexed and its identity stable; the malformed YAML remains the user's to fix.skippedunreadable-file channel. Safest write posture, but the note drops out of (or churns in) the index.(a) preserves the most value and matches the byte-honest philosophy; either way the fix should land with a props.rs property over arbitrary/invalid frontmatter (stamp-at-most-once; second reindex writes nothing) and the invalid-YAML case added to the #18 generators.
Refs
crates/b2-core/tests/props.rs)docs/design/invariants.md(stamp-once; index = pure projection)