diff --git a/DESIGN/02-architecture.md b/DESIGN/02-architecture.md index dad5de9..0086703 100644 --- a/DESIGN/02-architecture.md +++ b/DESIGN/02-architecture.md @@ -20,7 +20,7 @@ BoardStore (one per open board, @Observable, MainActor) SwiftUI views (board window + card windows share the store) ``` -One-way flow: **files → watcher → loader → store → views**. User actions go through a Writer that mutates files; the change comes back around through the watcher like any external edit. The app trusts its own writes no more than anyone else's — **for rendering** (settled scope): the snapshot is only ever built from disk, never from memory of what the app meant to write — this is what makes external editors and agents first-class. Provenance is a separate, downstream concern: the **EchoLedger** (Components below) remembers what the app wrote so commit attribution (06-history-undo.md) and VoiceOver announcements (10-accessibility.md) can tell the app's own echo from a foreign change — without the render path ever trusting memory over disk. +One-way flow: **files → watcher → loader → store → views**. User actions go through a Writer that mutates files; the change comes back around through the watcher like any external edit. The app trusts its own writes no more than anyone else's — **for rendering** (settled scope): the snapshot is only ever built from disk, never from memory of what the app meant to write — this is what makes external editors and agents first-class. Provenance is a separate, downstream concern: the **EchoLedger** (Components below) remembers what the app wrote so commit attribution (06-history-undo.md) and VoiceOver announcements (10-accessibility.md) can tell the app's own echo from a foreign change — without the render path ever trusting memory over disk. **The write path's do-nothing guards read the pending truth** (ruled 2026-08-06): never-trust-memory is the *render* path's scope, and it does not extend to a guard deciding whether a write would change disk. Between a write's bracket and its echo the snapshot describes the past — a guard comparing an asked-for value against it answers the wrong question, and a fast gesture pair silently loses its second half (the found case: two lane resizes released within one echo, A→B→A — the second compares A against the stale snapshot's A, writes nothing, and the first's echo settles the board at B, the width the user last dragged away from). So a do-nothing guard's baseline is the snapshot **as amended by this store's own in-flight writes** — the value it last wrote to that field and has not yet seen echo (a small pending-value record of the write path's own, *not* the EchoLedger, whose feeds-attribution-only charter stands); a gesture whose meaning is relative (step one width unit) resolves its base against the same amended truth, or a fast double-step loses its second press to the same staleness; and the undo step's recorded prior reads it too, or its inverse restores a state that never was. The amendment dies with its echo — a landed snapshot agreeing with the write clears it — and a failed or refused write never enters it (no echo is coming; the snapshot is still the truth). Per-gesture coalescing was weighed and set aside: one gesture is one write, one echo, one commit (the style batch's rule), and merging two gestures' writes would merge their commits. The **one named exception** is transient UI state rendering things that don't exist on disk — concretely the **new-card placeholder** (04-interactions.md): the inline editor for a card being created renders as a pseudo-card overlaid on the snapshot, with no disk presence and no UUID until the title commits. Commit creates the folder through the Writer and round-trips through the watcher like any write — the placeholder stays visible until the real card arrives, then hands off. **The handoff must read as one arrival** (settled): the placeholder renders at the arriving card's exact geometry — same slot, same size, same chrome — so the identity swap's cross-fade is imperceptible; two view identities are fine, two visible objects are not (no matched-geometry machinery across the overlay/snapshot boundary, just matched rendering). Abandoning (Escape, empty commit, click-away) discards it; disk was never touched. **A failed create discards it too** (settled): if the Writer create throws after the title commits, the create flow discards the placeholder — the failure surfaces as the ordinary one-shot banner (Write-failure surfacing below), and the overlay never waits for a card that cannot arrive. **Starting a new creation while a placeholder is open is a click-away for the draft** (settled): the open draft discards per its rule and the new placeholder begins — and ⌘N can't even reach this case (board commands disable while the editor is focused, 04-interactions.md), so only pointer paths do. Watcher reloads swap the snapshot *underneath* the overlay (like selection surviving a reload); if the placeholder's lane vanished in the reload, it is discarded — consistent with card windows dismissing when their card is deleted. Everything durable still round-trips through files.