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
+223 -2
View File
@@ -64,11 +64,15 @@ private func expectFixtureFailure(
do {
_ = try loadFixture(relativePath)
Issue.record("expected \(relativePath) to fail with \(reasonDescription) at '\(path)', but it loaded")
} catch let error as BoardLoadError {
} catch let failure as BoardLoadFailure {
// Each `Malformed/` board is minimal "one broken thing" so the aggregate holds exactly
// one defect, and asserting the count is what keeps that authoring rule true.
#expect(failure.defects.count == 1, "\(relativePath): expected one defect, got \(failure.defects)")
let error = failure.primary
#expect(error.path == path, "\(relativePath): wrong path in error")
#expect(matches(error.reason), "\(relativePath): expected \(reasonDescription), got \(error.reason)")
} catch {
Issue.record("\(relativePath): expected a BoardLoadError, got \(error)")
Issue.record("\(relativePath): expected a BoardLoadFailure, got \(error)")
}
}
@@ -563,3 +567,220 @@ struct FixtureMalformedTests {
}
}
}
// MARK: - Malformed/many-defects.kanban the collect-all board
/// **The loader collects every fail-fast defect in the walk rather than stopping at the first**
/// (01-storage-format.md § Malformed input, settled 2026-07-31 the decision surface's whole
/// premise: "one aggregated surface presents them all", never a chain of modals).
///
/// Three classes on one board, on real disk: the root's own missing `schema`, a card written by a
/// newer Lanework, and a lane whose frontmatter will not parse.
private enum ManyDefects {
static let board = "Malformed/many-defects.kanban"
static let intactLane = "10000000-0000-4000-8000-000000000001"
static let brokenLane = "30000000-0000-4000-8000-000000000002"
static let tailLane = "50000000-0000-4000-8000-000000000003"
static let intactCard = "20000000-0000-4000-8000-000000000001"
static let newerCard = "20000000-0000-4000-8000-000000000002"
/// Broken, and behind the broken lane never enumerated, so never in the aggregate.
static let cardBehindTheBrokenLane = "40000000-0000-4000-8000-000000000009"
static let tailCard = "60000000-0000-4000-8000-000000000004"
}
struct FixtureManyDefectsTests {
/// The whole list, in walk order, asserted as a list the ordering *is* the contract, because it
/// is what the surface groups top-down and what `primary` reads off.
@Test func everyFailFastDefectIsCollectedInWalkOrder() {
do {
_ = try loadFixture(ManyDefects.board)
Issue.record("a board with three fail-fast defects loaded")
} catch let failure as BoardLoadFailure {
// The paths, as a list, because the *order* is the contract: the root first its own
// `schema` is the this-really-is-a-board gate, and the walk continued past it because
// nothing below reads the root's document then lanes in folder-name order with each
// lane's cards inside it.
#expect(failure.defects.map(\.path) == [
"index.md",
"\(ManyDefects.intactLane)/\(ManyDefects.newerCard)/index.md",
"\(ManyDefects.brokenLane)/index.md",
])
guard failure.defects.count == 3 else { return }
#expect(failure.defects[0].reason == .missingSchema)
#expect(failure.defects[1].reason == .schemaNewerThanApp(found: 2))
// The lane's reason is matched by shape rather than by the parser's exact sentence,
// which is the YAML engine's wording and not this suite's to pin.
if case .unparseableYAML = failure.defects[2].reason {} else {
Issue.record("expected unparseable YAML on the lane, got \(failure.defects[2].reason)")
}
} catch {
Issue.record("expected a BoardLoadFailure, got \(error)")
}
}
/// **A broken lane takes its subtree with it, and that is the designed loop.** The card under the
/// unparseable lane is broken too and is deliberately absent from the aggregate: it was never
/// enumerated. Repair the lane, Re-check "re-runs the whole walk" and the next aggregate
/// carries it. Deeper defects surface one repair at a time, by design, not by omission.
@Test func aBrokenLanesSubtreeIsNeverEnumerated() {
do {
_ = try loadFixture(ManyDefects.board)
Issue.record("a board with three fail-fast defects loaded")
} catch let failure as BoardLoadFailure {
#expect(!failure.defects.contains { $0.path.contains(ManyDefects.cardBehindTheBrokenLane) })
} catch {
Issue.record("expected a BoardLoadFailure, got \(error)")
}
}
/// `primary` is the walk's first defect the outermost problem, which is the one a one-line
/// surface should name.
@Test func primaryIsTheRootsOwnDefect() {
do {
_ = try loadFixture(ManyDefects.board)
Issue.record("a board with three fail-fast defects loaded")
} catch let failure as BoardLoadFailure {
#expect(failure.primary == BoardLoadError(path: "index.md", reason: .missingSchema))
#expect(failure.description.hasSuffix("(and 2 more)"))
} catch {
Issue.record("expected a BoardLoadFailure, got \(error)")
}
}
/// **The root is unskippable** (01-storage-format.md § Malformed input): the surface never offers
/// Skip there the root defects have minted repairs, and a newer root schema is Cancel-only so
/// a set naming the root's `index.md` is ignored rather than obeyed. Policed in the loader, which
/// is what makes a board with no root document impossible to construct.
@Test func aSkipSetNamingTheRootIsIgnored() {
do {
_ = try BoardLoader.load(
boardRoot: fixtureBoard(ManyDefects.board),
skipping: [
"index.md",
".",
"\(ManyDefects.intactLane)/\(ManyDefects.newerCard)/index.md",
"\(ManyDefects.brokenLane)/index.md",
]
)
Issue.record("the root's own defect was skipped — a board with no schema loaded")
} catch let failure as BoardLoadFailure {
// Exactly the root's, and nothing else: the two below-root entries were honoured.
#expect(failure.defects == [BoardLoadError(path: "index.md", reason: .missingSchema)])
} catch {
Issue.record("expected a BoardLoadFailure, got \(error)")
}
}
}
// MARK: - Malformed/skippable-defects.kanban the skip channel
/// **"Skip is user-consented tolerance, loudly marked"** (01-storage-format.md § Malformed input,
/// ruled 2026-07-31): a skipped item loads the board without it, the file stays on disk untouched,
/// and the opened board carries a warning naming what left. Per-open, never persisted which this
/// suite gets for free, because the set is an argument.
private enum SkippableDefects {
static let board = "Malformed/skippable-defects.kanban"
static let intactLane = "10000000-0000-4000-8000-000000000001"
static let brokenLane = "30000000-0000-4000-8000-000000000002"
static let tailLane = "50000000-0000-4000-8000-000000000003"
static let intactCard = "20000000-0000-4000-8000-000000000001"
static let newerCard = "20000000-0000-4000-8000-000000000002"
static let cardBehindTheBrokenLane = "40000000-0000-4000-8000-000000000009"
static let tailCard = "60000000-0000-4000-8000-000000000004"
static let newerCardPath = "\(intactLane)/\(newerCard)/index.md"
static let brokenLanePath = "\(brokenLane)/index.md"
}
struct FixtureSkippableDefectsTests {
/// Unskipped, the board is an ordinary two-defect refusal the baseline the skips are measured
/// against, and the proof the fixture is broken in exactly two places.
@Test func withoutSkipsTheBoardRefusesWithBothDefects() {
do {
_ = try loadFixture(SkippableDefects.board)
Issue.record("a board with two fail-fast defects loaded")
} catch let failure as BoardLoadFailure {
#expect(failure.defects.map(\.path) == [
SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath,
])
} catch {
Issue.record("expected a BoardLoadFailure, got \(error)")
}
}
/// Skip both and the board opens **and each skipped item leaves with its whole subtree**. The
/// broken lane's own card is perfectly valid and is gone too: that is the honest cost of the
/// tolerance, and the reason the notice names what left rather than pretending nothing did.
@Test func skippingEveryDefectLoadsTheBoardWithoutThoseItems() throws {
let result = try BoardLoader.load(
boardRoot: fixtureBoard(SkippableDefects.board),
skipping: [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath]
)
#expect(result.model.title.value == "Skippable Defects")
#expect(result.model.lanes.map(\.id.rawValue) == [
SkippableDefects.intactLane, SkippableDefects.tailLane,
])
let intact = try #require(result.model.lanes.first { $0.id.rawValue == SkippableDefects.intactLane })
#expect(intact.cards.map(\.id.rawValue) == [SkippableDefects.intactCard])
// The subtree went with the lane, valid card and all.
let everyCard = result.model.lanes.flatMap { $0.cards.map(\.id.rawValue) }
#expect(!everyCard.contains(SkippableDefects.cardBehindTheBrokenLane))
#expect(everyCard == [SkippableDefects.intactCard, SkippableDefects.tailCard])
}
/// The loud mark: one warning per skip, naming the defect's own path which is what the surface
/// row named and what the opened board's notice resolves its Reveal in Finder against.
@Test func everySkipIsWarnedAbout() throws {
let result = try BoardLoader.load(
boardRoot: fixtureBoard(SkippableDefects.board),
skipping: [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath]
)
#expect(result.warnings == [
.userSkipped(path: SkippableDefects.newerCardPath),
.userSkipped(path: SkippableDefects.brokenLanePath),
])
}
/// A partial skip is an ordinary refusal over what is left the surface's per-item override,
/// and the reason Skip is a set rather than a switch.
@Test func skippingOneDefectStillRefusesForTheOther() {
do {
_ = try BoardLoader.load(
boardRoot: fixtureBoard(SkippableDefects.board),
skipping: [SkippableDefects.newerCardPath]
)
Issue.record("the unskipped lane defect did not refuse the load")
} catch let failure as BoardLoadFailure {
#expect(failure.defects.map(\.path) == [SkippableDefects.brokenLanePath])
} catch {
Issue.record("expected a BoardLoadFailure, got \(error)")
}
}
/// **Skip touches nothing on disk** "the file stays on disk untouched, tolerated-invisible like
/// strays". Asserted on the one tree where a stray write would show up in `git status`.
@Test func aSkippedItemIsLeftExactlyWhereItIs() throws {
let root = fixtureBoard(SkippableDefects.board)
let before = try allIndexMdFiles(under: root).map(\.path).sorted()
_ = try BoardLoader.load(
boardRoot: root,
skipping: [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath]
)
#expect(try allIndexMdFiles(under: root).map(\.path).sorted() == before)
let skipped = root.appendingPathComponent(SkippableDefects.brokenLanePath)
let text = try String(contentsOf: skipped, encoding: .utf8)
#expect(text.contains("labels: [red, green"), "the skipped file was rewritten")
}
}