Memoize the reload parse and short-circuit value-equal snapshots

The loader gains a ParseMemo — the previous walk's parsed documents keyed
by root-relative path, trusted on the git-index heuristic (mtime + size,
no hashing) and passed as an input so the loader stays stateless. A hit
skips exactly one file read; schema, order, coercions, dedupe, and every
directory listing run fresh, so memoized and cold walks are output-
identical (golden-corpus equivalence suite). Entries record only past the
schema gate, so a defect can never be answered from the memo.

The store skips the snapshot assignment wholesale when the fresh model is
value-equal — no @Observable churn, no render pass, no snapshotGeneration
bump — and a new landedReloads counter carries walk-completion for the
three consumers whose subject is the walk, not the snapshot: the card
window's comment thread, the comment search index, and the auto-committer's
covering gate (which now counts a completed walk as covering even when
nothing changed). Warnings and defects move on their own equality; failed
reloads bump neither counter. An injectable ParseCounter makes the
single-file-echo claim a test.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 11:38:25 -04:00
parent 5e6417e749
commit 988a7245a3
11 changed files with 1085 additions and 70 deletions
+230
View File
@@ -602,3 +602,233 @@ struct BoardStoreTests {
#expect(store.reloadGeneration == 0)
}
}
// MARK: - The parse memo and the value-equal skip
/// **"The walk memoizes its parse, never its result"** and **"the store skips the assignment entirely
/// when the fresh snapshot equals the current one"** (02-architecture.md § Live-reload resilience,
/// both blessed 2026-07-31).
///
/// The loader's half is pinned in `BoardLoaderParseMemoTests` and over the golden corpus in
/// `FixtureMemoEquivalenceTests`; what only this suite can state is the *wiring* that the store
/// hands each walk the one before it (`BoardStore.parseCounter` is the seam that makes "re-parsed
/// nothing" observable at all), and that the skip is a skip of the observable assignment and of
/// nothing else the landing owes.
@MainActor
@Suite("BoardStore ▸ the parse memo and the value-equal skip")
struct BoardStoreReloadMemoTests {
/// Every `index.md` `makeBoard()` puts in the walk's way: the root, two lanes, two cards. The
/// stray `notes/` folder holds one too and is deliberately not counted a non-UUID-shaped folder
/// is never descended into, so the loader never opens it, memo or no memo.
static let indexCount = 5
/// One reload through the store's one inbound door, with a counter attached.
private func reload(_ store: BoardStore, _ origin: WatchOrigin = .foreign) async -> BoardLoader.ParseCounter.Counts {
let counter = BoardLoader.ParseCounter()
store.parseCounter = counter
store.handleWatcherEvent(.treeChanged(origin))
await store.awaitQuiescence()
store.parseCounter = nil
return counter.counts
}
@Test("A reload of an untouched tree opens no index.md and assigns no snapshot")
func anUnchangedTreeCostsNothing() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let before = store.snapshot
let counts = await reload(store)
#expect(counts == .init(parsed: 0, reused: Self.indexCount))
// The skip proper: no assignment, so no `@Observable` churn and no render pass.
#expect(store.snapshotGeneration == 0)
#expect(store.snapshot == before)
// And the landing still happened the bookkeeping the skip must never cover.
#expect(store.landedReloads == 1)
#expect(store.reloadFailure == nil)
}
@Test("A single-file echo re-parses exactly that file, and does assign")
func oneEditedFileIsTheOnlyOneReParsed() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "Renamed"))
let counts = await reload(store)
#expect(counts == .init(parsed: 1, reused: Self.indexCount - 1))
#expect(cardTitles(inLane: Ident.lane1, of: store.snapshot) == ["Renamed", "Second"])
#expect(store.snapshotGeneration == 1)
#expect(store.landedReloads == 1)
}
@Test("The memo carries from reload to reload rather than going cold every other walk")
func theMemoChains() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
// Three landings: one value-equal (which skips the assignment), one that changes a file, and
// one value-equal again. If the skipped landing dropped its memo, the walk after it would go
// cold which is exactly the bug this pins.
#expect(await reload(store) == .init(parsed: 0, reused: Self.indexCount))
try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Edited"))
#expect(await reload(store) == .init(parsed: 1, reused: Self.indexCount - 1))
#expect(await reload(store) == .init(parsed: 0, reused: Self.indexCount))
#expect(cardTitles(inLane: Ident.lane1, of: store.snapshot) == ["First", "Edited"])
#expect(store.snapshotGeneration == 1, "only the middle reload had anything to assign")
#expect(store.landedReloads == 3)
#expect(store.parseMemo.count == Self.indexCount)
}
@Test("A failed reload keeps the snapshot and bumps neither counter")
func aFailedReloadBumpsNothing() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let before = store.snapshot
try fixture.item(Ident.lane1, brokenIndex)
_ = await reload(store)
#expect(store.snapshot == before, "a failed reload never replaces a good snapshot")
#expect(store.snapshotGeneration == 0)
#expect(store.landedReloads == 0, "there is no snapshot in hand, so nothing was covered")
#expect(store.reloadFailure?.primary.path == indexPath(Ident.lane1))
}
@Test("A failed reload's breakage clears on the next success, which is value-equal")
func breakageClearsOnAValueEqualSuccess() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
try fixture.item(Ident.lane1, brokenIndex)
_ = await reload(store)
#expect(store.reloadFailure != nil)
// Repaired to exactly what it was: the model comes back value-equal, the assignment is
// skipped and the standing breakage condition must still heal, because it is not board
// structure and is not what the skip covers.
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
_ = await reload(store)
#expect(store.reloadFailure == nil)
#expect(store.snapshotGeneration == 0, "the repaired tree is the tree that was already on screen")
#expect(store.landedReloads == 1)
}
// MARK: What the skip must never cover
@Test("Warnings follow the tree even when the model does not move")
func warningsFollowTheTree() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
// A second non-UUID-shaped folder: a stray is not in the model, so this changes `loadWarnings`
// and nothing else. It is also never descended into, so the walk still opens no file.
try fixture.item("scratch", "not a board item at all\n")
let counts = await reload(store)
#expect(counts == .init(parsed: 0, reused: Self.indexCount))
#expect(store.snapshotGeneration == 0)
#expect(store.landedReloads == 1)
#expect(store.loadWarnings.contains(.nonUUIDFolderIgnored(path: "scratch")))
#expect(store.loadWarnings.contains(.nonUUIDFolderIgnored(path: "notes")))
}
@Test("Defects follow the tree even when the model does not move")
func defectsFollowTheTree() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
#expect(store.looseCardFiles.isEmpty)
// A loose file beside an untouched `index.md`: pending work the walk found, with the snapshot
// value-equal on either side of it. Defects are what `runScheduledHeals` reads, so a skip
// that swallowed them would silently retire the heal engine on quiet boards.
try "loose".write(
to: fixture.url("\(Ident.lane1)/\(Ident.card1)").appendingPathComponent("notes.txt"),
atomically: true,
encoding: .utf8
)
_ = await reload(store)
#expect(store.snapshotGeneration == 0)
#expect(store.landedReloads == 1)
#expect(store.looseCardFiles.map(\.fileNames) == [["notes.txt"]])
}
@Test("An attachment arriving moves the snapshot, because the snapshot carries the listing")
func attachmentsAreNotMemoized() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let attachments = fixture.url("\(Ident.lane1)/\(Ident.card1)")
.appendingPathComponent("attachments", isDirectory: true)
try FileManager.default.createDirectory(at: attachments, withIntermediateDirectories: true)
try "png".write(to: attachments.appendingPathComponent("shot.png"), atomically: true, encoding: .utf8)
let counts = await reload(store)
// Not one file opened, and the card's paperclip is still current: directory enumeration is
// outside the memo's scope by design, because an attachment never touches `index.md`.
#expect(counts == .init(parsed: 0, reused: Self.indexCount))
#expect(lane(Ident.lane1, in: store.snapshot)?.cards.first?.attachments == ["shot.png"])
#expect(store.snapshotGeneration == 1)
#expect(store.landedReloads == 1)
}
@Test("A no-change reload announces nothing and leaves no state behind")
func aNoChangeReloadIsSilent() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
var spoken: [String?] = []
store.announce = { spoken.append($0) }
_ = await reload(store)
_ = await reload(store, .reconciling)
#expect(spoken == [nil, nil])
#expect(store.readOnlyLock == nil)
#expect(store.snapshotGeneration == 0)
#expect(store.landedReloads == 2)
}
@Test("A value-equal reload still lands for the auto-commit seam and the covering gate")
func aValueEqualReloadStillLands() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let landings = LandingCount()
store.commitSeam = HistoryCommitSeam(
willWrite: {},
writeBracketDidClose: {},
reloadDidLand: { _ in landings.value += 1 }
)
_ = await reload(store)
// The covering gate counts landings, not assignments (`GitAutoCommitter.landedReloads`), and
// the commit seam is armed by every landing whether or not the snapshot moved "a landing
// that finds nothing to commit is the silent no-op, not a wasted trip".
#expect(landings.value == 1)
#expect(store.landedReloads == 1)
#expect(store.snapshotGeneration == 0)
}
}
/// What the reload path told the history seam a box, because `HistoryCommitSeam` is a struct of
/// closures and a captured `var` cannot be read back after the reload has landed.
@MainActor
private final class LandingCount {
var value = 0
}