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
+309
View File
@@ -1233,3 +1233,312 @@ struct BoardLoaderCoercionTraceTests {
])
}
}
// MARK: - The parse memo
private extension BoardFixture {
/// A file's modification date, forced. Used wherever a test rewrites bytes without changing the
/// byte *count*: the stamp rule is the subject there, and leaving it to the filesystem clock
/// would make the assertion a race against timestamp resolution rather than a statement about
/// mtime.
func setModified(_ relativePath: String, to date: Date) throws {
let folder = relativePath.isEmpty ? root : root.appendingPathComponent(relativePath, isDirectory: true)
try FileManager.default.setAttributes(
[.modificationDate: date],
ofItemAtPath: folder.appendingPathComponent("index.md").path
)
}
}
/// A board with every container the walk has: a root, two lanes, two cards in the first, one
/// attachment, and one trashed card so "the whole tree came out of the memo" is a claim about all
/// four levels rather than about lanes.
private func memoBoard() throws -> (fixture: BoardFixture, lane: String, card: String) {
let fixture = try BoardFixture()
let lane = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
let card = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
try fixture.index("", "schema: 1\ntitle: Board\n")
try fixture.index(lane, "schema: 1\norder: 1024\ntitle: Todo\n")
try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ntitle: First\n")
try fixture.index("\(lane)/cccccccc-cccc-4ccc-8ccc-cccccccccccc", "schema: 1\norder: 2048\ntitle: Second\n")
try fixture.index(".trash/dddddddd-dddd-4ddd-8ddd-dddddddddddd", "schema: 1\norder: 1024\ntitle: Gone\n")
try fixture.strayFile("\(lane)/\(card)/attachments/shot.png", contents: "png")
return (fixture, lane, card)
}
/// Every `index.md` the board above holds the number a warm walk must answer for without opening
/// one of them.
private let memoBoardIndexCount = 5
/// A whole-second modification date, forced onto a file wherever a test needs two stamps to compare
/// *exactly*. A date read back off the filesystem does not necessarily round-trip through
/// `setAttributes` bit for bit (`Date` is a `Double` and the syscall's is a `timespec`), and a test
/// about mtime equality must not turn into a test about that conversion.
private let pinnedMtime = Date(timeIntervalSince1970: 1_750_000_000)
/// **The walk memoizes its parse, never its result** (02-architecture.md § Live-reload resilience,
/// blessed 2026-07-31 `BoardLoader.ParseMemo`).
///
/// Two claims that pull in opposite directions, which is why they are pinned side by side. The memo
/// has to actually save the reads: a walk over an untouched tree opens no `index.md` at all. And it
/// has to be undetectable in the answer, for the files it saved and harder for everything it
/// deliberately does not cover, which is every directory listing the walk makes. The equivalence half
/// is stated again over the golden fixture boards (`FixtureMemoEquivalenceTests`); this suite pins the
/// mechanism file by file, where a synthetic tree can be edited between two walks.
@Suite("BoardLoader ▸ the parse memo")
struct BoardLoaderParseMemoTests {
@Test("A cold walk parses every index.md and reuses nothing")
func aColdWalkParsesEverything() throws {
let (fixture, _, _) = try memoBoard()
defer { fixture.tearDown() }
let counter = BoardLoader.ParseCounter()
let result = try BoardLoader.load(boardRoot: fixture.root, counter: counter)
#expect(counter.counts == .init(parsed: memoBoardIndexCount, reused: 0))
#expect(result.memo.count == memoBoardIndexCount)
}
@Test("A walk over an untouched tree opens no index.md at all")
func anUntouchedTreeIsAnsweredEntirelyFromTheMemo() throws {
let (fixture, _, _) = try memoBoard()
defer { fixture.tearDown() }
let cold = try BoardLoader.load(boardRoot: fixture.root)
let counter = BoardLoader.ParseCounter()
let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)
#expect(counter.counts == .init(parsed: 0, reused: memoBoardIndexCount))
// The whole of the claim's other half: the cheap walk and the cold one are the same walk.
#expect(warm.model == cold.model)
#expect(warm.warnings == cold.warnings)
#expect(warm.defects == cold.defects)
#expect(warm.trashKinds == cold.trashKinds)
#expect(warm.memo.count == cold.memo.count)
}
@Test("A single-file echo re-parses exactly that file")
func oneEditedFileIsTheOnlyOneReParsed() throws {
let (fixture, lane, card) = try memoBoard()
defer { fixture.tearDown() }
let cold = try BoardLoader.load(boardRoot: fixture.root)
try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ntitle: Renamed\n")
let counter = BoardLoader.ParseCounter()
let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)
#expect(counter.counts == .init(parsed: 1, reused: memoBoardIndexCount - 1))
#expect(warm.model.lanes.first?.cards.first?.title.value == "Renamed")
// And the cold answer is still the answer: the memo changed the cost, nothing else.
#expect(warm.model == (try BoardLoader.load(boardRoot: fixture.root).model))
}
@Test("A same-size rewrite still re-parses, because the mtime moved")
func aSameSizeRewriteReParses() throws {
let (fixture, lane, card) = try memoBoard()
defer { fixture.tearDown() }
try fixture.setModified("\(lane)/\(card)", to: pinnedMtime)
let cold = try BoardLoader.load(boardRoot: fixture.root)
// "First" "Third": identical byte count, so `size` alone would call this unchanged.
try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ntitle: Third\n")
try fixture.setModified("\(lane)/\(card)", to: pinnedMtime.addingTimeInterval(1))
let counter = BoardLoader.ParseCounter()
let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)
#expect(counter.counts == .init(parsed: 1, reused: memoBoardIndexCount - 1))
#expect(warm.model.lanes.first?.cards.first?.title.value == "Third")
}
/// **The heuristic's stated blind spot, pinned rather than discovered** (02-architecture.md: "The
/// mtime+size trust is the git-index heuristic; a writer that defeats it content changed, mtime
/// and size both preserved is outside the app's care").
///
/// It is here so the boundary is a decision with a test on it: this is the one shape in which a
/// memoized walk and a cold walk disagree, and the design says so out loud.
@Test("A rewrite that preserves both mtime and size is trusted — the git-index heuristic's edge")
func aRewritePreservingTheStampIsTrusted() throws {
let (fixture, lane, card) = try memoBoard()
defer { fixture.tearDown() }
try fixture.setModified("\(lane)/\(card)", to: pinnedMtime)
let cold = try BoardLoader.load(boardRoot: fixture.root)
try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ntitle: Third\n")
try fixture.setModified("\(lane)/\(card)", to: pinnedMtime)
let counter = BoardLoader.ParseCounter()
let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)
#expect(counter.counts == .init(parsed: 0, reused: memoBoardIndexCount))
#expect(warm.model.lanes.first?.cards.first?.title.value == "First")
// And the very next walk that *does* see a moved stamp catches up the window is one write,
// not a standing state.
try fixture.setModified("\(lane)/\(card)", to: pinnedMtime.addingTimeInterval(1))
let next = try BoardLoader.load(boardRoot: fixture.root, memo: warm.memo)
#expect(next.model.lanes.first?.cards.first?.title.value == "Third")
}
// MARK: Directory enumeration is never memoized
@Test("An attachment arriving is seen by a walk that parsed nothing")
func attachmentListingsStayFresh() throws {
let (fixture, lane, card) = try memoBoard()
defer { fixture.tearDown() }
let cold = try BoardLoader.load(boardRoot: fixture.root)
// An attachment never touches `index.md`, which is exactly why the memo must not cover the
// listing that finds it.
try fixture.strayFile("\(lane)/\(card)/attachments/second.png", contents: "png")
let counter = BoardLoader.ParseCounter()
let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)
#expect(counter.counts == .init(parsed: 0, reused: memoBoardIndexCount))
#expect(warm.model.lanes.first?.cards.first?.attachments == ["second.png", "shot.png"])
}
@Test("A loose file arriving beside an untouched index.md is still a defect")
func looseFileDetectionStaysFresh() throws {
let (fixture, lane, card) = try memoBoard()
defer { fixture.tearDown() }
let cold = try BoardLoader.load(boardRoot: fixture.root)
#expect(cold.looseCardFiles.isEmpty)
try fixture.strayFile("\(lane)/\(card)/notes.txt", contents: "loose")
let counter = BoardLoader.ParseCounter()
let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)
#expect(counter.counts == .init(parsed: 0, reused: memoBoardIndexCount))
#expect(warm.looseCardFiles.map(\.fileNames) == [["notes.txt"]])
}
@Test("A new card folder is discovered, and it is the only file the walk opens")
func folderDiscoveryStaysFresh() throws {
let (fixture, lane, _) = try memoBoard()
defer { fixture.tearDown() }
let cold = try BoardLoader.load(boardRoot: fixture.root)
let arrival = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"
try fixture.index("\(lane)/\(arrival)", "schema: 1\norder: 4096\ntitle: Arrived\n")
let counter = BoardLoader.ParseCounter()
let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)
#expect(counter.counts == .init(parsed: 1, reused: memoBoardIndexCount))
#expect(warm.model.lanes.first?.cards.map(\.title.value) == ["First", "Second", "Arrived"])
}
@Test("A vanished card leaves the model, and its memo entry goes with it")
func aVanishedItemLeavesTheMemo() throws {
let (fixture, lane, card) = try memoBoard()
defer { fixture.tearDown() }
let cold = try BoardLoader.load(boardRoot: fixture.root)
try FileManager.default.removeItem(at: fixture.root.appendingPathComponent("\(lane)/\(card)"))
let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo)
#expect(warm.model.lanes.first?.cards.map(\.title.value) == ["Second"])
#expect(warm.memo.count == memoBoardIndexCount - 1)
}
// MARK: Defects can never be answered from it
@Test("A defective index.md is never memoized, so a still-broken file is re-read every walk")
func aDefectIsNeverMemoized() throws {
let (fixture, lane, card) = try memoBoard()
defer { fixture.tearDown() }
let healthy = try BoardLoader.load(boardRoot: fixture.root)
try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\nlabels: [a, b\n")
// The file does not change between these two walks, and neither of them can go quiet about
// it: a defective `index.md` is never recorded, so there is nothing for a later walk to hit.
// The healthy memo still spares the four files that *did* load, which is the point the
// defect costs one read, not a cold walk.
for _ in 0..<2 {
let counter = BoardLoader.ParseCounter()
do throws(BoardLoadFailure) {
_ = try BoardLoader.load(boardRoot: fixture.root, memo: healthy.memo, counter: counter)
Issue.record("expected the broken card to fail the walk")
} catch {
#expect(error.defects.map(\.path) == ["\(lane)/\(card)/index.md"])
}
#expect(counter.counts == .init(parsed: 1, reused: memoBoardIndexCount - 1))
}
// And the aggregate is the cold aggregate, defect for defect.
do throws(BoardLoadFailure) {
_ = try BoardLoader.load(boardRoot: fixture.root, memo: healthy.memo)
Issue.record("expected the broken card to fail the walk")
} catch let warm {
do throws(BoardLoadFailure) {
_ = try BoardLoader.load(boardRoot: fixture.root)
Issue.record("expected the broken card to fail the walk")
} catch let cold {
#expect(warm.defects == cold.defects)
}
}
}
@Test("A repaired file re-parses and rejoins the board")
func aRepairedFileReParses() throws {
let (fixture, lane, card) = try memoBoard()
defer { fixture.tearDown() }
let healthy = try BoardLoader.load(boardRoot: fixture.root)
try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\nlabels: [a, b\n")
try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ntitle: Repaired\n")
let warm = try BoardLoader.load(boardRoot: fixture.root, memo: healthy.memo)
#expect(warm.model.lanes.first?.cards.first?.title.value == "Repaired")
}
@Test("A skip is recomputed from a fresh parse, memo or no memo")
func aSkipComposesWithTheMemo() throws {
let (fixture, lane, card) = try memoBoard()
defer { fixture.tearDown() }
let healthy = try BoardLoader.load(boardRoot: fixture.root)
try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\nlabels: [a, b\n")
let skips: Set<String> = ["\(lane)/\(card)/index.md"]
let counter = BoardLoader.ParseCounter()
let warm = try BoardLoader.load(
boardRoot: fixture.root, skipping: skips, memo: healthy.memo, counter: counter)
let cold = try BoardLoader.load(boardRoot: fixture.root, skipping: skips)
// A skipped path is a defect path, so it was never in the memo and is read on every walk.
#expect(counter.counts.parsed == 1)
#expect(warm.model == cold.model)
#expect(warm.warnings == cold.warnings)
#expect(warm.warnings.contains(.userSkipped(path: "\(lane)/\(card)/index.md")))
// The skipped item is out of the board and out of the memo, both walks alike.
#expect(warm.memo.count == memoBoardIndexCount - 1)
}
@Test("A skipped path stays skipped across a memoized reload")
func aSkipHoldsAcrossReloads() throws {
let (fixture, lane, card) = try memoBoard()
defer { fixture.tearDown() }
let healthy = try BoardLoader.load(boardRoot: fixture.root)
try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\nlabels: [a, b\n")
let skips: Set<String> = ["\(lane)/\(card)/index.md"]
let first = try BoardLoader.load(boardRoot: fixture.root, skipping: skips, memo: healthy.memo)
let second = try BoardLoader.load(boardRoot: fixture.root, skipping: skips, memo: first.memo)
#expect(second.model == first.model)
#expect(second.warnings == first.warnings)
#expect(second.model.lanes.first?.cards.map(\.title.value) == ["Second"])
}
}