The loader collects every fail-fast defect and honors per-open skips

Phase 1 of the decision surface (01 ▸ Malformed input, settled
2026-07-31): BoardLoadFailure aggregates the walk's defects in walk
order — stop-at-first retires. Environmental failures (unreadable root,
not-a-directory) stay immediate single-defect throws: there is no walk
to collect from. A defective root index is recorded and the walk
continues into the children (nothing in the walk consults the parsed
root document — verified); a defective lane, card, or trash-entry index
records and skips its subtree, Re-check's whole-walk re-aggregation
being the designed loop for what hides beneath. load(skipping:) is the
per-open skip channel: a skipped path's item is omitted from the model
and surfaces as LoadWarning.userSkipped; root paths are unskippable by
construction. The reload-breakage banner carries the aggregate ("…and
N more"), single-defect sentences byte-identical to before. Two new
multi-defect fixture boards; suite 2591 green.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 09:12:49 -04:00
parent 94e60cd444
commit ba1726fa77
35 changed files with 897 additions and 117 deletions
+17 -15
View File
@@ -279,12 +279,13 @@ public final class BoardStore: HealHost {
defects.compactMap { if case let .duplicateIdentity(work) = $0 { work } else { nil } }
}
/// The standing read-side condition: the error from the last reload that failed, `nil` when the
/// board is healthy. `BoardLoadError` already carries fail-fast's specifics the offending path
/// and what is wrong with it which is the whole of what the banner needs to render
/// (02-architecture.md § Live-reload resilience). This is a *condition*, not a one-shot: it
/// stands until a reload succeeds, and it heals without ceremony when one does.
public private(set) var reloadFailure: BoardLoadError?
/// The standing read-side condition: the failure from the last reload that failed, `nil` when the
/// board is healthy. `BoardLoadFailure` carries every fail-fast defect that walk found each one
/// the offending path and what is wrong with it which is the whole of what the banner needs to
/// render (02-architecture.md § Live-reload resilience; the banner shows `primary` and counts the
/// rest). This is a *condition*, not a one-shot: it stands until a reload succeeds, and it heals
/// without ceremony when one does.
public private(set) var reloadFailure: BoardLoadFailure?
/// Non-`nil` while the board refuses writes. Cleared by the next successful reload, per "the
/// next successful reload clears both the banner and the lock".
@@ -582,8 +583,9 @@ public final class BoardStore: HealHost {
/// Opens a board: one synchronous tree walk, and **no fallback if it fails**.
///
/// Fail-fast is the *initial-load* contract (01-storage-format.md § Malformed input): there is
/// no last-good snapshot to keep on screen yet, so a broken board throws its `BoardLoadError`
/// instead of constructing a store that would have nothing to show. Every rule below the
/// no last-good snapshot to keep on screen yet, so a broken board throws its `BoardLoadFailure`
/// every fail-fast defect the walk found, aggregated instead of constructing a store that
/// would have nothing to show. Every rule below the
/// banner, the lock, "a failed reload never replaces a good snapshot" exists only *because*
/// this one succeeded.
///
@@ -596,7 +598,7 @@ public final class BoardStore: HealHost {
/// with a reload behind it rather than a write into a board nothing is watching yet. A store
/// built directly (a test, a storeless consumer) heals when it is asked to, and on every reload
/// thereafter.
public init(rootURL: URL) throws(BoardLoadError) {
public init(rootURL: URL) throws(BoardLoadFailure) {
let result = try BoardLoader.load(boardRoot: rootURL)
self.rootURL = rootURL
self.snapshot = result.model
@@ -676,10 +678,10 @@ public final class BoardStore: HealHost {
Self.logger.debug("reload \(generation, privacy: .public) started (\(origin.rawValue, privacy: .public))")
Task.detached(priority: .userInitiated) { [weak self] in
// `do throws(BoardLoadError)`: without the annotation the `catch` widens to `any Error`
// and the loader's typed error is lost on the way into `Result`.
let outcome: Result<LoadResult, BoardLoadError>
do throws(BoardLoadError) {
// `do throws(BoardLoadFailure)`: without the annotation the `catch` widens to `any Error`
// and the loader's typed failure is lost on the way into `Result`.
let outcome: Result<LoadResult, BoardLoadFailure>
do throws(BoardLoadFailure) {
outcome = .success(try BoardLoader.load(boardRoot: root, historyRanker: historyRanker))
} catch {
outcome = .failure(error)
@@ -690,7 +692,7 @@ public final class BoardStore: HealHost {
}
/// Lands one walk's result and starts whatever it uncovered.
private func apply(_ outcome: Result<LoadResult, BoardLoadError>, generation: Int, origin: WatchOrigin) {
private func apply(_ outcome: Result<LoadResult, BoardLoadFailure>, generation: Int, origin: WatchOrigin) {
reloadInFlight = false
// The stale-apply guard. Serialization means this should not trigger today, but "only the
@@ -704,7 +706,7 @@ public final class BoardStore: HealHost {
resumeQuiescenceWaitersIfQuiet()
}
private func land(_ outcome: Result<LoadResult, BoardLoadError>, generation: Int, origin: WatchOrigin) {
private func land(_ outcome: Result<LoadResult, BoardLoadFailure>, generation: Int, origin: WatchOrigin) {
// Consumed here, before the branch, because *both* outcomes end the expectation: a wholesale
// operation gets exactly one reload to prove itself, and a second failure after it is
// ordinary per-file breakage again.