Build BoardStore — one-way flow and resilient reloads

Per-board @Observable MainActor hub (Kanban/LiveStore/): watcher signals
drive off-main tree walks with a generation guard, single-flight
coalescing (strongest pending origin, watcher's merge rule), and the
resilience contract — a failed reload never replaces a good snapshot,
per-file breakage never locks editing, and a wholesale operation
(performWholesale) arms a reload-must-succeed-or-lock floor so a failed
post-bracket reload flips the board read-only until a good reload heals
it. Selection is a pure UUID-set value re-resolved on every swap;
liveness flips eject. performWrite brackets the watcher so Writer
round-trips come back app-mediated.

14 store tests; full suite 279 tests in 54 suites green. Three findings
filed on the Redesign board.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
2026-07-26 19:14:15 -04:00
parent d32f03f8c9
commit 66516d38d1
3 changed files with 1013 additions and 2 deletions
+7 -2
View File
@@ -28,7 +28,7 @@ public enum WatchOrigin: String, Sendable, Equatable {
/// reload has to cover" a reconciling reload assumes nothing about the tree, an
/// app-mediated one is the tail of an operation the app ran, a foreign one is an observed
/// external edit.
fileprivate var precedence: Int {
var precedence: Int {
switch self {
case .foreign: 0
case .appMediated: 1
@@ -41,7 +41,12 @@ public enum WatchOrigin: String, Sendable, Equatable {
/// This is the *never downgrade* rule a foreign event landing on a pending app-mediated or
/// reconciling delivery does not weaken it. See `FolderWatcher.schedule(_:)` for why the
/// resulting blur is accepted rather than engineered away.
fileprivate static func merged(_ existing: WatchOrigin?, _ incoming: WatchOrigin) -> WatchOrigin {
///
/// Internal rather than `fileprivate`: `BoardStore` coalesces signals that arrive while a reload
/// is already running and owes the same never-downgrade guarantee on its side of the handoff.
/// Two coalescing points, one rule the watcher's debounce and the store's pending-reload flag
/// must never disagree about which origin a merged span carries.
static func merged(_ existing: WatchOrigin?, _ incoming: WatchOrigin) -> WatchOrigin {
guard let existing else { return incoming }
return existing.precedence >= incoming.precedence ? existing : incoming
}