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
+29 -8
View File
@@ -244,8 +244,10 @@ public enum BannerRow: Identifiable, Sendable {
/// vanished root, and the writability probe at open and, symmetrically, on every reconciling
/// reload thereafter.
case readOnlyLock(ReadOnlyLockReason)
/// A reload failed and the last good snapshot is still on screen. Condition, error tone.
case reloadBreakage(BoardLoadError)
/// A reload failed and the last good snapshot is still on screen. Condition, error tone. Carries
/// the **whole** aggregate one row either way, but its headline names the first defect and
/// counts the rest rather than pretending the walk found only one.
case reloadBreakage(BoardLoadFailure)
/// A write that did not happen. Dismissable, error tone.
case oneShot(OneShotBanner)
/// A git operation that did not happen an undo restore, a branch switch, and (pro-m2) a pull
@@ -813,7 +815,7 @@ public final class BannerCenter {
/// Finder drop that skipped folders already posts one (`postSkippedFolders`).
public nonisolated static func rows(
lock: ReadOnlyLockReason?,
breakage: BoardLoadError?,
breakage: BoardLoadFailure?,
oneShots: [OneShotBanner],
losses: [LossBanner],
suspension: HistorySuspension?,
@@ -1154,12 +1156,31 @@ public final class BannerCenter {
///
/// The path is root-relative as `BoardLoadError` reports it, and `"."` the root's own
/// `index.md` is spelled as "This board" rather than shown as a lone dot.
public nonisolated static func headline(for breakage: BoardLoadError) -> String {
let reason = trimmed(breakage.reason.description)
let subject = breakage.path == "." || breakage.path.isEmpty
///
/// **One defect is named, the rest are counted** (01-storage-format.md § Malformed input: the
/// loader collects every fail-fast defect in a walk). A banner is one line and a list of paths
/// is the first thing that would truncate, so the sentence stays the sentence it always was
/// the walk's first defect, said in full with ", and N more" between the reason and the
/// reassurance. The full list is not lost: it is the decision surface's to show on the next
/// attended open, which is where a repair is actually made.
///
/// A single-defect failure reads **exactly** as it did before the aggregate existed.
public nonisolated static func headline(for breakage: BoardLoadFailure) -> String {
breakageHeadline(breakage.primary, others: breakage.defects.count - 1)
}
/// One defect's own sentence the same rule, for the surfaces that hold exactly one and know
/// it: the template chooser's unloadable row, whose folder is picked from rather than opened.
public nonisolated static func headline(for defect: BoardLoadError) -> String {
breakageHeadline(defect, others: 0)
}
private nonisolated static func breakageHeadline(_ defect: BoardLoadError, others: Int) -> String {
let subject = defect.path == "." || defect.path.isEmpty
? "This board isn't loading"
: "'\(breakage.path)' isn't loading"
return "\(subject): \(reason) — showing the last good view"
: "'\(defect.path)' isn't loading"
let more = others > 0 ? ", and \(others) more" : ""
return "\(subject): \(trimmed(defect.reason.description))\(more) — showing the last good view"
}
/// The skipped-folders line 04-interactions.md's own example, "Folders can't be attached 2