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:
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user