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
+6 -1
View File
@@ -889,8 +889,13 @@ public final class AppModel {
// await are reads of the store the composer is already diffing, which is why they are
// wired here rather than reached for: the engine holds the *policy* (when to wait, how
// long), the session supplies the two facts (`GitAutoCommitter.awaitCoveringSnapshot`).
//
// The generation the gate counts is `landedReloads` completed *walks* rather than
// applied snapshots because a value-equal reload skips the assignment and its
// counter since 2026-07-31, and a walk covers a flush's paths whether or not it found
// anything to change (`BoardStore.landedReloads`).
committer.awaitReloadQuiescence = { [weak store] in await store?.awaitQuiescence() }
committer.snapshotGeneration = { [weak store] in store?.snapshotGeneration }
committer.landedReloads = { [weak store] in store?.landedReloads }
// 02-architecture.md Write-failure surfacing, through the strip the board window
// already renders: a genuine commit failure means "your edits are saved, history has
// stopped advancing", which is exactly what the standing suspension row says. Lock
+11 -5
View File
@@ -484,13 +484,19 @@ struct CardWindowHost: View {
attachments.isEditable = !locked
session.comments.isEditable = !locked
}
// **The thread re-reads on every applied snapshot** (05 The comments column: "the pane
// **The thread re-reads on every landed reload** (05 The comments column: "the pane
// reloads its thread from the same FSEvents stream").
//
// *Any* reload, not a filtered one, and that is a deliberate choice worth stating: the
// store's observable surface publishes `snapshotGeneration` and a `BoardModel` it does
// not vend the changed paths, and comments are outside the snapshot entirely
// (01-storage-format.md § Enhanced schema), so there is nothing to filter *on* here.
// store's observable surface publishes counters and a `BoardModel` it does not vend the
// changed paths, and comments are outside the snapshot entirely (01-storage-format.md
// § Enhanced schema), so there is nothing to filter *on* here.
//
// **`landedReloads`, not `snapshotGeneration`**, and for that same sentence's reason: a
// comment arriving changes the tree and leaves the model value-equal, and a value-equal
// landing skips the snapshot assignment (blessed 2026-07-31). Watching the applied counter
// would mean the one kind of change this pane exists to notice is the one kind it would
// sleep through.
// Re-reading one card's thread is a handful of small files and happens only while a card
// window is open; filtering would mean either widening the store's surface to carry paths,
// or the pane keeping its own watcher a second stream over the same tree, which the
@@ -499,7 +505,7 @@ struct CardWindowHost: View {
// ledger's comment receipts through `CommentPath.classify` to tell a foreign arrival from
// its own echo (`CardComments.reload`). `initial:` is deliberately absent: `start()`
// already did the opening read, after the residue sweep that has to precede it.
.onChange(of: store.snapshotGeneration) { _, _ in
.onChange(of: store.landedReloads) { _, _ in
session.comments.reload()
}
} else {