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
+11 -4
View File
@@ -142,14 +142,21 @@ public final class GitAutoCommitter {
@ObservationIgnored
public var awaitReloadQuiescence: (@MainActor () async -> Void)?
/// **Which generation the board `currentSnapshot` answers with is at**
/// `BoardStore.snapshotGeneration`, incremented by every landed reload.
/// **How many tree walks the board has landed** `BoardStore.landedReloads`, incremented by
/// every reload that completed with a snapshot in hand.
///
/// **The walk, not the applied snapshot**, and the distinction is load-bearing since 2026-07-31:
/// a reload whose tree turned out to be value-equal skips the snapshot assignment and its counter
/// (02-architecture.md § Live-reload resilience), and a gate watching *that* counter would sit out
/// its whole deadline on a flush whose covering walk had already landed. What covers a flush is a
/// walk that started after its writes reached disk, and a
/// completed walk covers them whether or not it found anything different to show.
///
/// `nil` the closure absent, or answering `nil` because the store has gone means there is no
/// snapshot to be outrun by, and the covering await becomes the no-op it is on every storeless
/// committer.
@ObservationIgnored
public var snapshotGeneration: (@MainActor () -> Int?)?
public var landedReloads: (@MainActor () -> Int?)?
/// **How long an explicit flush waits for its covering reload** before composing from the snapshot
/// it already has.
@@ -563,7 +570,7 @@ public final class GitAutoCommitter {
/// after the later of them 06's own "practical cushion", doing the job it is enough for and
/// `noteWillWrite()` cannot await at all, being the synchronous flush-before-overwrite.
private func awaitCoveringSnapshot() async {
guard holdsUncoveredWrites, let read = snapshotGeneration else { return }
guard holdsUncoveredWrites, let read = landedReloads else { return }
await awaitReloadQuiescence?()
// Re-read the gate: the quiescence may itself have been the covering landing.
guard holdsUncoveredWrites, let base = read() else { return }