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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user