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
+36 -6
View File
@@ -97,7 +97,7 @@ struct BannerCenterOrderingTests {
let rows = BannerCenter.rows(
lock: .vanishedRoot,
breakage: BoardLoadError(path: "todo/index.md", reason: .missingOrder),
breakage: BoardLoadFailure(BoardLoadError(path: "todo/index.md", reason: .missingOrder)),
oneShots: [attachment, move],
losses: [loss],
suspension: HistorySuspension(reason: "disk full", since: Date(timeIntervalSince1970: 50)),
@@ -322,7 +322,7 @@ struct BannerCenterLifecycleTests {
// heal", 02 § The banner surface), and this is where it is stated as a test.
let rows = BannerCenter.rows(
lock: .bracketedReloadFailed,
breakage: BoardLoadError(path: ".", reason: .boardRootMissingIndex),
breakage: BoardLoadFailure(BoardLoadError(path: ".", reason: .boardRootMissingIndex)),
oneShots: center.oneShots,
losses: [],
suspension: HistorySuspension(reason: "disk full"),
@@ -603,7 +603,7 @@ struct BannerRowControlsTests {
func conditionRowsCarryNoControls() {
let rows: [BannerRow] = [
.readOnlyLock(.vanishedRoot),
.reloadBreakage(BoardLoadError(path: "Todo/index.md", reason: .missingOrder)),
.reloadBreakage(BoardLoadFailure(BoardLoadError(path: "Todo/index.md", reason: .missingOrder))),
.historySuspended(HistorySuspension(reason: "the repository is corrupt")),
]
@@ -618,7 +618,7 @@ struct BannerRowControlsTests {
func inventoryAgreesWithDismissID() {
let rows: [BannerRow] = [
.readOnlyLock(.vanishedRoot),
.reloadBreakage(BoardLoadError(path: "Todo/index.md", reason: .missingOrder)),
.reloadBreakage(BoardLoadFailure(BoardLoadError(path: "Todo/index.md", reason: .missingOrder))),
.oneShot(OneShotBanner(error: error(.move(title: "Fix login")))),
.gitFailure(GitFailureBanner(operation: .branchSwitch, reason: "the repository is locked")),
.loss(LossBanner(message: "Pasted 'Fix login' without its 3 attachments")),
@@ -790,7 +790,9 @@ struct BannerCenterPhrasingTests {
@Test("Reload breakage carries fail-fast's specifics — the path and what is wrong with it")
func breakageHeadlineNamesThePath() {
let headline = BannerCenter.headline(
for: BoardLoadError(path: "todo/fix-login/index.md", reason: .unparseableYAML(message: "unexpected end", line: 4))
for: BoardLoadFailure(BoardLoadError(
path: "todo/fix-login/index.md",
reason: .unparseableYAML(message: "unexpected end", line: 4)))
)
#expect(headline.contains("'todo/fix-login/index.md'"))
#expect(headline.contains("line 4"))
@@ -798,11 +800,39 @@ struct BannerCenterPhrasingTests {
// The board's own index.md reports as "." a lone dot in the product's voice would be a
// bug report, not a sentence.
let rootHeadline = BannerCenter.headline(for: BoardLoadError(path: ".", reason: .boardRootMissingIndex))
let rootHeadline = BannerCenter.headline(
for: BoardLoadFailure(BoardLoadError(path: ".", reason: .boardRootMissingIndex)))
#expect(!rootHeadline.contains("'.'"))
#expect(rootHeadline.hasPrefix("This board isn't loading"))
}
/// **One defect named, the rest counted** (01-storage-format.md § Malformed input the loader
/// collects every fail-fast defect in a walk). A banner is one line, so the sentence stays the
/// sentence it always was and the remainder rides as a count; the full list is the decision
/// surface's to show on the next attended open.
///
/// Both spellings are pinned, because the single-defect one is what every existing surface reads
/// and it must not have drifted when the aggregate arrived.
@Test("A multi-defect breakage names the first and counts the rest")
func breakageHeadlineCountsTheRest() {
let first = BoardLoadError(path: "index.md", reason: .missingSchema)
let second = BoardLoadError(path: "todo/index.md", reason: .schemaNewerThanApp(found: 2))
let third = BoardLoadError(path: "done/index.md", reason: .malformedSchema(raw: "one"))
#expect(BannerCenter.headline(for: BoardLoadFailure([first]))
== "'index.md' isn't loading: missing required 'schema' field — showing the last good view")
#expect(BannerCenter.headline(for: BoardLoadFailure([first, second]))
== "'index.md' isn't loading: missing required 'schema' field, and 1 more — showing the last good view")
#expect(BannerCenter.headline(for: BoardLoadFailure([first, second, third]))
== "'index.md' isn't loading: missing required 'schema' field, and 2 more — showing the last good view")
// The row that carries it says the same thing the headline is not re-derived anywhere.
#expect(BannerRow.reloadBreakage(BoardLoadFailure([first, second])).headline
== BannerCenter.headline(for: BoardLoadFailure([first, second])))
}
@Test("The suspended-history line names the consequence, then the diagnosis")
func suspensionHeadlineNamesTheConsequence() {
#expect(BannerCenter.headline(for: HistorySuspension(reason: "the disk is full"))