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
+105
View File
@@ -784,3 +784,108 @@ struct FixtureSkippableDefectsTests {
#expect(text.contains("labels: [red, green"), "the skipped file was rewritten")
}
}
// MARK: - Memo-vs-cold equivalence, over every golden board
/// **Result-purity with cost unspecified** (02-architecture.md § Live-reload resilience, blessed
/// 2026-07-31: "The loader's contract is result-purity with cost unspecified: same tree in, same
/// snapshot out, and the memo can only change how fast").
///
/// The memo is the one thing in the loader that could make two walks of the same tree disagree, so
/// the claim is stated where the trees are real and hand-authored: every golden board is walked cold,
/// then walked again with the first walk's memo, and the two results are compared whole. Nothing here
/// edits a tree the mechanism's per-file behaviour is `BoardLoaderParseMemoTests`' subject; this is
/// the equivalence, across every shape the fixture corpus holds.
///
/// The malformed boards are included on purpose. A refusal is a result too, and a memo that changed
/// which defects a walk collected or their order would be the worst possible way for this to be
/// wrong, since the decision surface is written directly against that list.
struct FixtureMemoEquivalenceTests {
/// Every board under `Fixtures/Valid`, by relative path.
static let validBoards = [
"Valid/rich-board.kanban",
"Valid/interrupted-create.kanban",
"Valid/non-uuid-strays.kanban",
"Valid/stray-files.kanban",
"Valid/tombstones.kanban",
"Valid/duplicate-order-tie-break.kanban",
"Valid/unknown-key-order.kanban",
"Valid/coercion.kanban",
"Valid/duplicate-top-level-keys.kanban",
"Valid/board-level-deleted.kanban",
"Valid/optional-keys.kanban",
]
/// Every board under `Fixtures/Malformed`, by relative path.
static let malformedBoards = [
"Malformed/board-root-missing-index.kanban",
"Malformed/missing-schema.kanban",
"Malformed/unparseable-yaml.kanban",
"Malformed/schema-newer-than-app.kanban",
"Malformed/many-defects.kanban",
"Malformed/skippable-defects.kanban",
]
@Test("A memoized walk of a valid board is the cold walk, whole", arguments: validBoards)
func aMemoizedWalkMatchesTheColdWalk(board: String) throws {
let cold = try loadFixture(board)
let warm = try BoardLoader.load(boardRoot: fixtureBoard(board), memo: cold.memo)
#expect(warm.model == cold.model)
#expect(warm.warnings == cold.warnings)
#expect(warm.defects == cold.defects)
#expect(warm.trashKinds == cold.trashKinds)
// The memo the second walk produced can stand in for the first's, which is what makes the
// store's chain of reloads self-sustaining rather than degrading walk by walk.
#expect(warm.memo.count == cold.memo.count)
}
@Test("A memoized walk of a valid board opens no index.md at all", arguments: validBoards)
func aMemoizedWalkReadsNothing(board: String) throws {
let cold = try loadFixture(board)
let counter = BoardLoader.ParseCounter()
_ = try BoardLoader.load(boardRoot: fixtureBoard(board), memo: cold.memo, counter: counter)
#expect(counter.counts.parsed == 0)
#expect(counter.counts.reused == cold.memo.count)
#expect(counter.counts.reused > 0, "a fixture with nothing to memoize proves nothing")
}
@Test("A memoized walk of a malformed board refuses identically", arguments: malformedBoards)
func aMemoizedWalkRefusesIdentically(board: String) {
// A refusing walk hands back no memo that is the design, not an omission: a defective file
// is never recorded, and a walk that threw produced no `LoadResult` to carry one on. So the
// memo under test is the empty one a first walk would offer, and what is pinned is that the
// memo parameter never moves the aggregate a refusal reports.
let defects = refusal(board, memo: nil)
#expect(defects == refusal(board, memo: BoardLoader.ParseMemo()))
#expect(!defects.isEmpty, "\(board) is in the malformed corpus but loaded")
}
/// One malformed board's aggregate, or `[]` where it unexpectedly loaded.
private func refusal(_ board: String, memo: BoardLoader.ParseMemo?) -> [BoardLoadError] {
do throws(BoardLoadFailure) {
_ = try BoardLoader.load(boardRoot: fixtureBoard(board), memo: memo)
return []
} catch {
return error.defects
}
}
/// The malformed corpus' real memo case: a board that refuses is repaired-by-skip into one that
/// loads, and the memo that produced carries into the next walk without moving the answer.
@Test("A skipped-open board reloads through its own memo unchanged")
func aSkippedOpenReloadsUnchanged() throws {
let skips: Set<String> = [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath]
let root = fixtureBoard(SkippableDefects.board)
let cold = try BoardLoader.load(boardRoot: root, skipping: skips)
let warm = try BoardLoader.load(boardRoot: root, skipping: skips, memo: cold.memo)
#expect(warm.model == cold.model)
#expect(warm.warnings == cold.warnings)
#expect(warm.defects == cold.defects)
}
}