Write the loader fixture suite, valid and malformed

18 golden fixture boards as real on-disk trees under Fixtures/ — ten
valid/tolerated cases (rich board, interrupted create, non-UUID strays,
stray files, tombstones, tie-breaks, unknown-key order, coercion,
last-wins duplicates, board-level deleted) and eight fail-fast cases,
each pinned to its exact error reason and path; the rich board's whole
tree round-trips byte-identically. Pins two contract subtleties now
recorded in the design: tombstone flags are not inherited (renderers
walk ancestors) and last-wins resolves before order strictness. README
gains the storage-foundation feature entry. 146 tests total.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
2026-07-26 16:20:47 -04:00
parent e6c34c0ccf
commit 7470255886
73 changed files with 900 additions and 4 deletions
+2 -2
View File
@@ -81,7 +81,7 @@ Body: lane description / WIP policy / notes.
Body: the card's content — the whole point.
Schema-owned display fields are Lanework's to interpret — **coerce where a sensible reading exists, fall back to the field's default where none does** (settled). A scalar of the wrong YAML type reads as its source text (`title: 2048` displays as "2048", `width: "2"` reads as 2); where no sensible reading exists — a sequence or mapping where a scalar belongs, a non-integer width, an unparseable timestamp — the field falls back to its default: untitled placeholder, width 1, no color, no icon. Coercion is read-side only; the bytes on disk are **preserved verbatim, never rewritten**. One tombstone nuance: any *present* `deleted:` key tombstones the item — an unusable timestamp still deletes, its date merely unknown (the user's intent to delete outranks the broken date). **Duplicate keys: last one wins** (settled — the coercing read; strict YAML would reject the file, so this is a deliberate divergence in the editor's favor): a *top-level* key appearing twice reads as its last occurrence, earlier occurrences preserved verbatim on disk and invisible. A duplicate inside a nested mapping value remains a YAML error — the rescue applies where the editor's slip actually happens, the top level. An app write of a duplicated key rewrites the winning (last) occurrence and removes the earlier ones — the app owns the keys it writes, and leaving a stale twin would resurrect it if the winner were later removed; removing a key removes all its occurrences. **App rewrites preserve comments** (settled): comments on their own lines always survive a rewrite; an inline comment on a rewritten value line is re-spliced after the new value — best-effort, guaranteed for plain scalar lines (the realistic case), dropped only in pathological shapes. Fail-fast remains reserved for structure (`schema`, `order`, YAML validity) — and it covers *malformed*, not just missing: an `order` that is present but non-numeric is the same loud malformed-input rejection as a missing one. **Readable-but-uneditable shapes load; writes to them refuse** (settled): frontmatter the surgical editor can't key by spans — a whole-frontmatter flow mapping, non-scalar keys — reads and renders normally, and any app write to that file fails loudly through the per-file write-failure banner (02-architecture.md ▸ Write-failure surfacing) naming the shape, never a silent corruption and never a load rejection.
Schema-owned display fields are Lanework's to interpret — **coerce where a sensible reading exists, fall back to the field's default where none does** (settled). A scalar of the wrong YAML type reads as its source text (`title: 2048` displays as "2048", `width: "2"` reads as 2); where no sensible reading exists — a sequence or mapping where a scalar belongs, a non-integer width, an unparseable timestamp — the field falls back to its default: untitled placeholder, width 1, no color, no icon. Coercion is read-side only; the bytes on disk are **preserved verbatim, never rewritten**. One tombstone nuance: any *present* `deleted:` key tombstones the item — an unusable timestamp still deletes, its date merely unknown (the user's intent to delete outranks the broken date). **Duplicate keys: last one wins** (settled — the coercing read; strict YAML would reject the file, so this is a deliberate divergence in the editor's favor): a *top-level* key appearing twice reads as its last occurrence, earlier occurrences preserved verbatim on disk and invisible. A duplicate inside a nested mapping value remains a YAML error — the rescue applies where the editor's slip actually happens, the top level. Last-wins resolves *before* strictness: a duplicated `order` reads as its final occurrence and only that value faces the malformed-order check — duplication itself is never a fail-fast. An app write of a duplicated key rewrites the winning (last) occurrence and removes the earlier ones — the app owns the keys it writes, and leaving a stale twin would resurrect it if the winner were later removed; removing a key removes all its occurrences. **App rewrites preserve comments** (settled): comments on their own lines always survive a rewrite; an inline comment on a rewritten value line is re-spliced after the new value — best-effort, guaranteed for plain scalar lines (the realistic case), dropped only in pathological shapes. Fail-fast remains reserved for structure (`schema`, `order`, YAML validity) — and it covers *malformed*, not just missing: an `order` that is present but non-numeric is the same loud malformed-input rejection as a missing one. **Readable-but-uneditable shapes load; writes to them refuse** (settled): frontmatter the surgical editor can't key by spans — a whole-frontmatter flow mapping, non-scalar keys — reads and renders normally, and any app write to that file fails loudly through the per-file write-failure banner (02-architecture.md ▸ Write-failure surfacing) naming the shape, never a silent corruption and never a load rejection.
## Enhanced schema (reserved, out of scope)
@@ -108,7 +108,7 @@ Carried over unchanged — gapped fractional ranks:
## Deletion — tombstones
- Deleting writes `deleted: <ISO-8601>` into frontmatter; tombstoned items (and everything beneath) are hidden from the board and surface in the **trash quasi-lane** (03-board-ui.md) — a pure view. **Tombstoned items stay exactly where they are on disk**, which is what makes restore (Put Back = removing the key) position-preserving: the item reappears at its recorded `order` among its current siblings. One acknowledged edge: a renumber rescales *visible* siblings only (tombstones are inert to ordering), so a post-renumber Put Back can land among different neighbors than the ones it was deleted between — same lane, deterministic placement, just not always the original gap.
- Deleting writes `deleted: <ISO-8601>` into frontmatter; tombstoned items (and everything beneath) are hidden from the board. **Hiding beneath is the renderer's walk, not a stored flag** (pinned by the fixture suite): the snapshot flags only the item carrying `deleted:` — a live card inside a tombstoned lane stays `isDeleted == false`, and every consumer of the model (board view, trash, search, counts) must treat ancestor tombstones as hiding the subtree rather than filtering on the item flag alone and surface in the **trash quasi-lane** (03-board-ui.md) — a pure view. **Tombstoned items stay exactly where they are on disk**, which is what makes restore (Put Back = removing the key) position-preserving: the item reappears at its recorded `order` among its current siblings. One acknowledged edge: a renumber rescales *visible* siblings only (tombstones are inert to ordering), so a post-renumber Put Back can land among different neighbors than the ones it was deleted between — same lane, deterministic placement, just not always the original gap.
- Tombstones are **inert to ordering**: appends and renumbering operate on visible siblings only; a restored item re-enters at its recorded `order`, ties broken deterministically.
- A folder that disappears without a tombstone (Finder deletion) is also a delete; the app reflects it (such items are gone entirely — they never enter the trash).
- A `deleted:` key at **board level** is legal per the common frontmatter table but meaningless — a board can't tombstone itself out of its own window. The loader ignores it and logs (the lenient-values philosophy: preserved verbatim, invisible in the UI).