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
@@ -0,0 +1 @@
there is no board without an index.md
@@ -0,0 +1,7 @@
---
schema: 1
order:
title: Lane With Null Order
---
The hand-editor started the 'order' key and never gave it a value — reads
as missing, not malformed.
@@ -0,0 +1,4 @@
---
schema: 1
title: Board Is Fine
---
@@ -0,0 +1,5 @@
---
schema: 1
title: Card Missing Order
---
No 'order' key — required on every card.
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: Lane Is Fine
---
@@ -0,0 +1,4 @@
---
schema: 1
title: Board Is Fine
---
@@ -0,0 +1,5 @@
---
schema: 1
title: Lane Missing Order
---
No 'order' key — required on every lane.
@@ -0,0 +1,4 @@
---
schema: 1
title: Board Is Fine
---
@@ -0,0 +1,5 @@
---
title: A Board Missing Its Schema
---
No 'schema' field here — the loader fails fast before looking at anything
else.
@@ -0,0 +1,7 @@
---
schema: 1
order: banana
title: Card With Non-Numeric Order
---
'order' is present but not a number — malformed, not missing; the same
loud rejection as a missing value.
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: Lane Is Fine
---
@@ -0,0 +1,4 @@
---
schema: 1
title: Board Is Fine
---
@@ -0,0 +1,5 @@
---
schema: 2
title: From The Future
---
This board was written by a newer version of the app than this one.
@@ -0,0 +1,5 @@
---
schema: 1
bad: [1, 2
---
Body text.
+33 -1
View File
@@ -2,4 +2,36 @@
Golden fixture boards for the storage-contract test suite — real on-disk folder trees, not inline strings, so the same fixtures can later drive XCUITests via the `--open-board` launch hook.
Bundled into the unit-test target as a folder reference (see `project.yml`). Valid boards live under `Valid/`, fail-fast cases under `Malformed/`.
Bundled into the unit-test target as a folder reference (see `project.yml`). Valid boards live under `Valid/`, fail-fast cases under `Malformed/`. Tests live in `KanbanTests/FixtureBoardTests.swift`.
Lane/card folder names are fixed literal lowercase-UUIDv4-shaped strings (never generated at test time), chosen so their lexicographic order matches the expected tie-break order — usually a leading digit (`10000000-...`, `20000000-...`, …) so folder order reads the same as array-index order in the tests.
## Valid/ — one board per tolerated/valid case
| Board | Case |
|---|---|
| `rich-board.kanban` | A full-breadth well-formed board: 2 lanes, 3 cards, bodies, styling (background/icon/iconColor/width), unknown + reserved frontmatter keys, `attachments/` and `comments/` with real content. Also the board every `index.md` in the tree is round-tripped against. |
| `interrupted-create.kanban` | The motivating skip-not-error case: a UUID-shaped lane folder and a UUID-shaped card folder, each with no `index.md` yet (folder created, write not yet landed). |
| `non-uuid-strays.kanban` | Non-UUID-shaped folders at both lane and card depth, with and without `index.md` — name shape gates candidacy before the file is ever read. |
| `stray-files.kanban` | Stray (non-directory) files at board, lane, and card level — never level candidates, never warned about. |
| `tombstones.kanban` | A tombstoned lane and a tombstoned card, both still on disk and still in the snapshot, flagged (`isDeleted`) rather than removed. Also proves a tombstoned lane doesn't recursively flag its own un-deleted children. |
| `duplicate-order-tie-break.kanban` | Three cards sharing one `order` in one lane, and two lanes sharing one `order` — both broken by folder name, ascending. |
| `unknown-key-order.kanban` | Unknown/reserved frontmatter keys interleaved with schema-owned ones at board, lane, and card level — `document.unknownFields` must preserve exactly the order they were written in. |
| `coercion.kanban` | Lenient-field coercion and fallback: wrong-type scalars that coerce (`title: 2048`, `iconColor: 42`, `background: 12345`, `width: "3"`) versus ones with no sensible reading that fall back to the default (`title: [a, b]`, `background: {x: 1}`, `width: 1.5`), plus a `deleted` with an unusable timestamp that still tombstones. |
| `duplicate-top-level-keys.kanban` | A top-level key written twice — at board, lane (the strict `order` field), and card level. Last occurrence wins; **not** a fail-fast case (settled, newer than the original card text). Round-tripped to prove the earlier occurrence survives on disk, invisible only to reads. |
| `board-level-deleted.kanban` | A board-level `deleted:` key — legal per the frontmatter table but meaningless; ignored + warned, rest of the board loads normally. |
## Malformed/ — one board per fail-fast case
Each board is minimal: one broken thing.
| Board | Case |
|---|---|
| `unparseable-yaml.kanban` | An unterminated flow sequence in the board's frontmatter. |
| `missing-schema.kanban` | Board root `index.md` has no `schema` key. |
| `schema-newer-than-app.kanban` | Board root `schema: 2`, newer than `BoardLoader.supportedSchema`. |
| `missing-order-lane.kanban` | A UUID-shaped lane `index.md` with no `order` key. |
| `missing-order-card.kanban` | A UUID-shaped card `index.md` with no `order` key. |
| `explicit-null-order.kanban` | A lane's `order:` key present with no value — reads as missing, not malformed. |
| `non-numeric-order.kanban` | A card's `order: banana` — present but not a number. |
| `board-root-missing-index.kanban` | The board root folder itself has no `index.md`. |
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: Also Business As Usual
---
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: Business As Usual
---
@@ -0,0 +1,9 @@
---
schema: 1
title: Board With A Stray 'deleted' Key
deleted: 2026-01-01T00:00:00Z
---
'deleted' is legal per the common frontmatter table but meaningless at
board level — a board can't tombstone itself out of its own window. The
loader ignores it (and logs a warning) rather than acting on it; the rest
of the board loads completely normally.
@@ -0,0 +1,6 @@
---
schema: 1
order: 1024
title: 2048
---
An unquoted integer title coerces to its source text, "2048".
@@ -0,0 +1,7 @@
---
schema: 1
order: 2048
title: [a, b]
---
A sequence has no sensible string reading — malformed, falls back to the
untitled placeholder.
@@ -0,0 +1,7 @@
---
schema: 1
order: 3072
iconColor: 42
---
An integer iconColor coerces to its source text, "42" — not a real color,
but the value round-trips rather than being rejected.
@@ -0,0 +1,7 @@
---
schema: 1
order: 4096
background: {x: 1}
---
A mapping has no sensible string reading — malformed, falls back to no
color.
@@ -0,0 +1,5 @@
---
schema: 1
order: 5120
background: 12345
---
@@ -0,0 +1,8 @@
---
schema: 1
order: 6144
title: Deleted With An Unusable Timestamp
deleted: definitely-not-a-date
---
The user's intent to delete outranks the broken date — this card still
tombstones, its deletion date just unknown.
@@ -0,0 +1,6 @@
---
schema: 1
order: 1024
title: Width Coerces From A Quoted Number
width: "3"
---
@@ -0,0 +1,8 @@
---
schema: 1
order: 2048
title: Width Is Unusable — Not An Integer
width: 1.5
---
A malformed width is preserved on disk and simply not used — never
rejected, never rewritten.
+8
View File
@@ -0,0 +1,8 @@
---
schema: 1
title: Coercion And Lenient-Field Fallback
---
Schema-owned display fields coerce where a sensible reading exists (a
wrong-type scalar reads as its source text) and fall back to their default
where none does (a sequence/mapping, a non-integer width). A present but
unusable 'deleted' timestamp still tombstones — presence outranks validity.
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: Should Render First (folder starts with 1)
---
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: Should Render Second (folder starts with 2)
---
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: Should Render Third (folder starts with 3)
---
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: Lane With Three Tied Cards
---
@@ -0,0 +1,5 @@
---
schema: 1
order: 2048
title: Should Render Before Lane bbb... (same order, 'a' < 'b')
---
@@ -0,0 +1,5 @@
---
schema: 1
order: 2048
title: Should Render After Lane aaa... (same order, 'b' > 'a')
---
@@ -0,0 +1,7 @@
---
schema: 1
title: Duplicate Order Values, Tie-Broken By Folder Name
---
Two siblings that share an 'order' render in a deterministic order —
ascending 'order' first, folder name (lexicographic) as the tiebreak.
This board exercises the tiebreak at both lane level and card level.
@@ -0,0 +1,6 @@
---
schema: 1
order: 2048
title: First Title
title: Second Title
---
@@ -0,0 +1,9 @@
---
schema: 1
order: 1024
order: 4096
title: Dup Order Lane
---
Duplicate keys aren't special-cased by field — 'order' is a strict,
schema-owned field and still resolves via last-wins at the document layer,
before the loader ever validates it. This is NOT a fail-fast case.
@@ -0,0 +1,7 @@
---
schema: 1
title: Draft Name
title: Final Name
---
A hand-edit that types 'title' twice — strict YAML would reject the file
outright. The engine reads the last occurrence and keeps both on disk.
@@ -0,0 +1,6 @@
---
schema: 1
order: 1024
title: A Real Card
---
Body.
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: A Real Lane
---
@@ -0,0 +1,6 @@
---
schema: 1
title: Interrupted Two-Step Create
---
The app creates a folder first and writes index.md a moment later — a crash
or a killed app between those two steps must not brick the board.
@@ -0,0 +1,6 @@
---
schema: 1
order: 1024
title: Real Card
---
Body.
@@ -0,0 +1,5 @@
---
schema: 1
order: 9999
title: This looks like a card but isn't named like one
---
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: Real Lane
---
@@ -0,0 +1,7 @@
---
schema: 1
title: Non-UUID Strays At Every Depth
---
Name shape gates level detection — a folder that isn't UUIDv4-shaped is a
stray, whether or not it holds an index.md, and whether it sits at lane
depth or card depth.
@@ -0,0 +1,6 @@
---
schema: 1
order: 9999
title: This looks like a lane but isn't named like one
---
Never rendered — 'todo-notes' isn't UUIDv4-shaped.
@@ -0,0 +1,8 @@
---
schema: 1
author: rzen
created: 2026-07-21T08:30:00Z
---
Comments are enhanced-schema and out of scope for this version — this
folder is here only to prove it's preserved verbatim and never descended
into (cards are leaves; the loader never scans past index.md).
@@ -0,0 +1,17 @@
---
schema: 1
title: "Design the fixture taxonomy"
order: 1024
created: 2026-07-20T10:15:00Z
modified: 2026-07-26T20:07:30Z
modified-by: claude
source: DESIGN/01-storage-format.md # inline comment on a plain scalar
project: lanework
sphere: work
labels: [m1-storage-read]
---
Enumerate every fail-fast and tolerated case from the storage format and
build one golden fixture board per case.
- attachments/ holds a sketch
- comments/ holds a placeholder future-comment folder
@@ -0,0 +1,9 @@
---
schema: 1
title: Wire up the loader's stray tolerance
order: 2048
background: coral
icon: flag.fill
iconColor: orange
---
Plain, unquoted title this time — mixing styles on purpose.
@@ -0,0 +1,10 @@
---
schema: 1
order: 1024
title: Doing
width: 2
background: '#3478F6'
icon: hammer.fill
iconColor: blue
---
WIP limit: 3 cards. Pull from Backlog only when there's room.
@@ -0,0 +1,8 @@
---
schema: 1
order: 1024
title: 'Ship v1'
created: 2026-06-01T12:00:00Z
modified: 2026-07-15T09:00:00Z
---
Shipped. 🎉
@@ -0,0 +1,7 @@
---
schema: 1
order: 2048
title: Done
background: green
---
Completed work lives here until someone clears it out.
+22
View File
@@ -0,0 +1,22 @@
---
schema: 1
# board settings, hand-written
title: "Rich Demo Board"
created: 2026-07-01T09:00:00Z
modified: 2026-07-26T16:41:38Z
modified-by: claude
background: "#1E1E1E"
icon: rectangle.stack.fill
iconColor: purple
project: lanework # agent overlay
sphere: work
labels: [feature, docs]
template: {order: 3}
# trailing note before close
---
A demo board exercising the full breadth of the storage contract: several
lanes, several cards, body text, styling, and unknown/reserved frontmatter
keys living alongside the schema-owned ones.
Nothing here is special-cased — it's an ordinary board.
@@ -0,0 +1,6 @@
---
schema: 1
order: 1024
title: A Card With Junk Inside It
---
Body.
@@ -0,0 +1 @@
this whole subfolder is never scanned — cards are leaves
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: A Lane With Junk Beside It
---
@@ -0,0 +1 @@
board readme
@@ -0,0 +1,7 @@
---
schema: 1
title: Stray Files Everywhere
---
Stray files at board, lane, and card level are ignored — preserved
verbatim, never rendered, never warned about (only directories are level
candidates at all).
@@ -0,0 +1 @@
log line
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: A Living Card
---
@@ -0,0 +1,7 @@
---
schema: 1
order: 2048
title: A Tombstoned Card
deleted: 2026-07-10T08:00:00Z
---
Still on disk, at its recorded order, just hidden from the board.
@@ -0,0 +1,5 @@
---
schema: 1
order: 1024
title: Still Here
---
@@ -0,0 +1,7 @@
---
schema: 1
order: 1024
title: Not Itself Deleted
---
My lane is tombstoned but I am not — isDeleted keys only on my own
'deleted' field, never inherited from an ancestor.
@@ -0,0 +1,10 @@
---
schema: 1
order: 2048
title: A Tombstoned Lane
deleted: 2026-07-11T09:30:00Z
---
Everything beneath a tombstoned lane is hidden from the board too — but
that's a rendering rule, not a loader rule: the loader still reports this
lane's card structurally, un-flagged, since the card's own 'deleted' key
is absent.
@@ -0,0 +1,7 @@
---
schema: 1
title: Tombstoned Lane And Card Still On Disk
---
Deletion writes 'deleted:' into frontmatter; the item never leaves disk.
The loader keeps tombstoned items in the snapshot, flagged — hiding them
from the board is a rendering concern, not a loading one.
@@ -0,0 +1,12 @@
---
schema: 1
labels: [bug, urgent]
order: 1024
assignees: [bob, carol]
title: Card With Unknowns
due: 2026-08-15
remote: {system: github, id: 42}
agent-scratch: notes here
---
Card body describing the bug. Unknown/reserved keys are interleaved with
schema-owned ones on purpose, to prove ordering survives the interleave.
@@ -0,0 +1,9 @@
---
schema: 1
order: 1024
remote-state: syncing
title: Lane With Unknowns
assignees: [alice]
due: 2026-08-01
custom: xyz
---
@@ -0,0 +1,12 @@
---
schema: 1
project: lanework
title: Unknown Key Ordering
sphere: work
template: {order: 5}
labels: [x, y, z]
custom-note: keep me
---
Unknown keys and their relative order are preserved verbatim, on every
level — this is load-bearing for agent overlays (project:, sphere:,
custom tags).