The decision surface — a refused open becomes a live repair, in place

Phase 3 of the decision surface, completing the card (01 ▸ Malformed
input, settled 2026-07-31). An attended open's fail-fast walk transforms
the loading window's content into one aggregated surface — never a
sheet, never a chain: defects grouped by class, each class stated once
with its files listed (Reveal in Finder + Open in Editor per row), a
class-level default preselected, per-item override behind a disclosure.
Only honest choices: YAML and malformed-schema get Editor + Re-check
(Skip below the root); newer-than-app gets Skip alone and blocks the
board at the root; the two root repairs — minted index, schema: 1 stamp
— are defaults. Repair and Open applies fixes in one store-less write
bracket and re-walks: clean proceeds, remainder re-aggregates into the
same surface. Cancel and ⌘W retire to welcome's row; restored opens
never see the surface at all (OpenOrigin rides the PendingOpen carrier).

Skips are per-open consent that rides the session — the store retains
the skip set and every reload passes it — and the opened board posts a
warning-tone notice naming what was left out, each item's Reveal riding
the banner strip's new reveal control. On Pro boards the repair bracket
binds its own EchoLedger, heal-marks everything, and the store adopts it
before the committer starts, so repairs land as one separate commit
authored Lanework Integrity — pinned end to end. Also fixed en route: a
retired loading window left its close interception installed and
returned false from windowShouldClose forever, blocking quit.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 10:52:02 -04:00
parent 0933ac1b01
commit 31fee00c73
17 changed files with 2448 additions and 107 deletions
+769
View File
@@ -0,0 +1,769 @@
import Foundation
import SwiftGitX
import Testing
@testable import Kanban
/// **The decision surface** (01-storage-format.md § Malformed input, settled 2026-07-31) the
/// grouping, the vocabulary, the two minted repairs, the skip channel's ride through the session, and
/// the attendance branch that decides whether any of it is reached at all.
///
/// Everything here is asked of values. The surface's rules live in `BoardDecisionSurfaceModel` and
/// its landing rule in a static on the host precisely so a suite with no window can hold the whole
/// decision in its hand: what a class offers, what it defaults to, when Repair and Open may be
/// pressed, and what a repair actually puts on disk.
// MARK: - Fixtures
/// A board whose root has no `schema` and one lane that will not parse the minimal two-class
/// aggregate, and the shape most of these cases need: one minted repair, one hand-edit.
@MainActor
private func makeTwoClassBoard() throws -> WriterFixture {
let fixture = try WriterFixture()
// No `schema` at the root the this-really-is-a-board gate, defect #1.
try fixture.item("", "---\ntitle: Two Classes\n---\nA board with two kinds of problem.\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Intact\norder: 1024\n---\n")
try fixture.item("\(Ident.lane1)/\(Ident.card1)", "---\nschema: 1\ntitle: Card\norder: 1024\n---\n")
// Unparseable YAML an unterminated flow sequence, defect #2.
try fixture.item(Ident.lane2, "---\nschema: 1\ntitle: Broken\norder: 2048\nlabels: [a, b\n---\n")
return fixture
}
/// A board whose only defects are **below the root** the skip channel's golden case.
@MainActor
private func makeSkippableBoard() throws -> WriterFixture {
let fixture = try WriterFixture()
try fixture.item("", "---\nschema: 1\ntitle: Skippable\n---\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Intact\norder: 1024\n---\n")
try fixture.item("\(Ident.lane1)/\(Ident.card1)", "---\nschema: 1\ntitle: Card\norder: 1024\n---\n")
// A card from a newer Lanework: unfixable, so Skip is the only offer.
try fixture.item("\(Ident.lane1)/\(Ident.card2)", "---\nschema: 2\ntitle: Newer\norder: 2048\n---\n")
// A lane that will not parse: Open in Editor + Re-check, or Skip.
try fixture.item(Ident.lane2, "---\nschema: 1\ntitle: Broken\norder: 2048\nlabels: [a, b\n---\n")
return fixture
}
/// The failure a board refuses with the input every model here is built from.
@MainActor
private func failure(of fixture: WriterFixture, skipping: Set<String> = []) throws -> BoardLoadFailure {
do throws(BoardLoadFailure) {
_ = try BoardLoader.load(boardRoot: fixture.root, skipping: skipping)
Issue.record("the fixture board loaded — it is supposed to refuse")
throw BoardLoadFailure(BoardLoadError(path: ".", reason: .notADirectory))
} catch {
return error
}
}
@MainActor
private func makeModel(_ fixture: WriterFixture) throws -> BoardDecisionSurfaceModel {
BoardDecisionSurfaceModel(failure: try failure(of: fixture), boardRoot: fixture.root)
}
// MARK: - Grouping and defaults
@MainActor
@Suite("Decision surface ▸ grouping and defaults")
struct BoardDecisionSurfaceGroupingTests {
@Test("Defects group by class, and the classes come in walk order")
func groupingIsByClassInWalkOrder() throws {
let fixture = try makeSkippableBoard()
defer { fixture.tearDown() }
let model = try makeModel(fixture)
// The walk is root, then lanes in folder-name order with their cards inside them so the
// newer-schema card (under lane 1) is met before the unparseable lane 2.
#expect(model.sections.map(\.defectClass) == [.newerSchema, .unreadableFrontmatter])
#expect(model.sections.allSatisfy { $0.rows.count == 1 })
}
@Test("One class with several files is one section, in walk order")
func oneClassIsOneSection() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [a\n---\n")
try fixture.item(Ident.lane2, "---\nschema: 1\ntitle: B\norder: 2048\nlabels: [b\n---\n")
let model = try makeModel(fixture)
// "each class section states the defect once, lists the affected files".
#expect(model.sections.count == 1)
let section = try #require(model.sections.first)
#expect(section.defectClass == .unreadableFrontmatter)
#expect(section.rows.map(\.id) == ["\(Ident.lane1)/index.md", "\(Ident.lane2)/index.md"])
}
@Test("Every class preselects its ruled default")
func classDefaultsAreTheRuledOnes() throws {
// The minted repairs, at the root where they occur.
#expect(BoardDefectClass.missingBoardIndex.choices(atPath: "index.md").first == .repair(.mintBoardIndex))
#expect(BoardDefectClass.missingRootSchema.choices(atPath: "index.md").first == .repair(.stampSchema))
// "Open in Editor + Re-check, **or** Skip" the posture leads, the tolerance follows.
#expect(
BoardDefectClass.unreadableFrontmatter.choices(atPath: "lane/index.md")
== [.editAndRecheck, .skip]
)
// "an unfixable row offering only Skip".
#expect(BoardDefectClass.newerSchema.choices(atPath: "lane/index.md") == [.skip])
}
@Test("malformedSchema is seated with the YAML family, root restriction and all")
func malformedSchemaJoinsTheYAMLFamily() {
// Redesign Gap bcdd1942: not one of the ruled four, and the same honest posture the app
// must not guess what `schema: banana` was meant to be.
#expect(BoardDefectClass(.malformedSchema(raw: "banana")) == .unreadableFrontmatter)
#expect(BoardDefectClass(.unparseableYAML(message: "x", line: 2)) == .unreadableFrontmatter)
// The root restriction it inherits: Editor + Re-check at the root, plus Skip below it.
#expect(BoardDefectClass.unreadableFrontmatter.choices(atPath: "index.md") == [.editAndRecheck])
}
@Test("A root defect is never offered Skip")
func theRootIsNeverSkippable() throws {
let fixture = try makeTwoClassBoard()
defer { fixture.tearDown() }
let model = try makeModel(fixture)
let rootRow = try #require(model.rows.first { $0.id == BoardLoader.indexFileName })
#expect(!rootRow.choices.contains(.skip), "there is no board without its root")
#expect(rootRow.choice == .repair(.stampSchema))
}
@Test("Every row states its path and the walk's own reason")
func rowsCarryPathAndSpecifics() throws {
let fixture = try makeTwoClassBoard()
defer { fixture.tearDown() }
let model = try makeModel(fixture)
let broken = try #require(model.rows.first { $0.id == "\(Ident.lane2)/index.md" })
#expect(broken.defect.reason.description.contains("unparseable YAML"))
// Reveal points at the file that is actually there; Open in Editor too.
#expect(broken.revealURL.lastPathComponent == BoardLoader.indexFileName)
#expect(broken.editURL != nil)
}
@Test("A root with no index.md has nothing to open, and reveals its folder")
func aMissingIndexRevealsTheFolder() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Orphan\norder: 1024\n---\n")
let model = try makeModel(fixture)
let row = try #require(model.rows.first)
#expect(row.defectClass == .missingBoardIndex)
#expect(row.editURL == nil, "Finder cannot open a file that is not there")
#expect(row.revealURL == fixture.root, "the folder is what the user needs to look at")
}
}
// MARK: - The choices, the override, and the enabling rule
@MainActor
@Suite("Decision surface ▸ choices and the enabling rule")
struct BoardDecisionSurfaceChoiceTests {
@Test("A class-level choice sets every row in the class")
func theClassChoiceSetsTheClass() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [a\n---\n")
try fixture.item(Ident.lane2, "---\nschema: 1\ntitle: B\norder: 2048\nlabels: [b\n---\n")
let model = try makeModel(fixture)
#expect(model.sections.first?.classChoice == .editAndRecheck)
model.choose(.skip, inClass: .unreadableFrontmatter)
#expect(model.sections.first?.classChoice == .skip)
#expect(model.skipSet == ["\(Ident.lane1)/index.md", "\(Ident.lane2)/index.md"])
}
@Test("A per-item override moves one row and leaves the class mixed")
func perItemOverrideLeavesTheClassMixed() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [a\n---\n")
try fixture.item(Ident.lane2, "---\nschema: 1\ntitle: B\norder: 2048\nlabels: [b\n---\n")
let model = try makeModel(fixture)
model.choose(.skip, forPath: "\(Ident.lane1)/index.md")
#expect(model.skipSet == ["\(Ident.lane1)/index.md"], "only the overridden row moved")
#expect(model.sections.first?.classChoice == nil, "the class has no single answer any more")
#expect(model.sections.first?.offersPerItemOverride == true)
}
@Test("A class-level choice a row cannot take leaves that row alone")
func aRootRowIgnoresAClassLevelSkip() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
// Unparseable YAML at the root *and* below it: one class, two offer sets.
try fixture.item("", "---\nschema: 1\ntitle: Board\nlabels: [a\n---\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [b\n---\n")
let model = try makeModel(fixture)
model.choose(.skip, inClass: .unreadableFrontmatter)
#expect(model.skipSet == ["\(Ident.lane1)/index.md"], "the root never joins a skip set")
let rootRow = try #require(model.rows.first { $0.id == BoardLoader.indexFileName })
#expect(rootRow.choice == .editAndRecheck)
}
@Test("Repair and Open enables only when every defect has an actionable resolution")
func repairAndOpenNeedsEveryDefectResolved() throws {
let fixture = try makeSkippableBoard()
defer { fixture.tearDown() }
let model = try makeModel(fixture)
// Out of the box the unparseable lane defaults to "fix it myself", which is not a resolution.
#expect(!model.canRepairAndOpen)
model.choose(.skip, inClass: .unreadableFrontmatter)
#expect(model.canRepairAndOpen, "a consented Skip is an actionable resolution")
#expect(model.skipSet.count == 2, "the newer-schema card defaults to Skip, its only offer")
}
@Test("Repair and Open enables on a board whose only defect has a minted repair")
func aMintedRepairAloneEnablesIt() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", "---\ntitle: No Schema\n---\n")
let model = try makeModel(fixture)
#expect(model.canRepairAndOpen)
#expect(model.plannedRepairs == [
BoardDecisionSurfaceModel.PlannedRepair(path: "index.md", repair: .stampSchema)
])
}
@Test("A root blocked by a newer schema disables Repair and Open outright")
func aRootBlockerDisablesRepairAndOpen() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
// "on the board root it blocks the whole board (Cancel is the only exit)".
try fixture.item("", "---\nschema: 99\ntitle: From The Future\n---\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Fine\norder: 1024\n---\n")
let model = try makeModel(fixture)
let row = try #require(model.rows.first)
#expect(row.isBlocking, "no repair, and no tolerance at the root")
#expect(row.choice == nil)
#expect(model.isRootBlocked)
#expect(!model.canRepairAndOpen)
}
@Test("The surface still shows everything a blocked board has wrong")
func aBlockedSurfaceStillShowsEverything() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", "---\nschema: 99\ntitle: From The Future\n---\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [a\n---\n")
let model = try makeModel(fixture)
// The user whose root was written by a newer Lanework is owed the whole picture what they
// do next depends on how much else is wrong.
#expect(model.sections.map(\.defectClass) == [.newerSchema, .unreadableFrontmatter])
#expect(model.rows.count == 2)
}
}
// MARK: - Re-aggregation
@MainActor
@Suite("Decision surface ▸ re-aggregation")
struct BoardDecisionSurfaceReaggregationTests {
@Test("A fresh walk refreshes the same surface and keeps the answers still standing")
func reaggregationKeepsStandingAnswers() throws {
let fixture = try makeTwoClassBoard()
defer { fixture.tearDown() }
let model = try makeModel(fixture)
// The user overrides the broken lane to Skip, then repairs the root by hand outside the app.
model.choose(.skip, inClass: .unreadableFrontmatter)
try fixture.item("", "---\nschema: 1\ntitle: Two Classes\n---\n")
model.reaggregate(try failure(of: fixture))
#expect(model.sections.map(\.defectClass) == [.unreadableFrontmatter], "the root's defect is gone")
#expect(model.skipSet == ["\(Ident.lane2)/index.md"], "the standing consent survived the re-walk")
#expect(model.canRepairAndOpen)
}
@Test("A defect the walk newly reveals arrives at its class default")
func newlyRevealedDefectsTakeTheirDefault() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [a\n---\n")
// Hidden behind the broken lane: a broken lane takes its subtree with it.
try fixture.item("\(Ident.lane1)/\(Ident.card1)", "---\nschema: 2\ntitle: Newer\norder: 1024\n---\n")
let model = try makeModel(fixture)
#expect(model.rows.count == 1, "the card under the broken lane is never enumerated")
// The lane is fixed by hand; Re-check reveals what it was hiding in the *same* surface.
model.choose(.skip, forPath: "\(Ident.lane1)/index.md")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\n---\n")
model.reaggregate(try failure(of: fixture))
#expect(model.rows.count == 1)
let revealed = try #require(model.rows.first)
#expect(revealed.id == "\(Ident.lane1)/\(Ident.card1)/index.md")
#expect(revealed.choice == .skip, "the newer-schema class's only offer, preselected")
}
}
// MARK: - The repairs
@MainActor
@Suite("Decision surface ▸ the minted repairs")
struct BoardDecisionSurfaceRepairTests {
@Test("Stamping schema writes schema: 1, stamps modified, and clears modified-by")
func stampingSchemaIsAnOrdinaryAppWrite() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", """
---
title: No Schema
created: 2026-01-01T09:00:00Z
modified: 2026-01-01T09:00:00Z
modified-by: claude
project: lanework
---
Body text.
""")
let model = try makeModel(fixture)
let outcome = BoardRepairRun.apply(model.plannedRepairs, boardRoot: fixture.root)
#expect(outcome.failure == nil)
let text = try fixture.indexText("")
#expect(text.contains("schema: 1"))
// "each repaired `index.md` is an ordinary app write (stamps `modified`, clears
// `modified-by`)".
#expect(!text.contains("modified-by"))
#expect(!text.contains("modified: 2026-01-01"))
// And the round trip is the Writer's own: unknown keys and the body survive.
#expect(text.contains("project: lanework"))
#expect(text.contains("Body text."))
// The board loads now, which is the whole test of a repair.
#expect(throws: Never.self) { try BoardLoader.load(boardRoot: fixture.root) }
}
@Test("Minting a board index creates one titled after the folder, at schema 1")
func mintingABoardIndexUsesTheFolderName() throws {
let outer = FileManager.default.temporaryDirectory
.appendingPathComponent("DecisionMint-\(UUID().uuidString)", isDirectory: true)
let root = outer.appendingPathComponent("Roadmap.kanban", isDirectory: true)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: outer) }
try Data("---\nschema: 1\ntitle: Todo\norder: 1024\n---\n".utf8)
.write(to: {
let lane = root.appendingPathComponent(Ident.lane1, isDirectory: true)
try? FileManager.default.createDirectory(at: lane, withIntermediateDirectories: true)
return lane.appendingPathComponent(BoardLoader.indexFileName)
}())
let aggregate: BoardLoadFailure
do throws(BoardLoadFailure) {
_ = try BoardLoader.load(boardRoot: root)
Issue.record("a folder with no index.md is not a board")
return
} catch {
aggregate = error
}
let model = BoardDecisionSurfaceModel(failure: aggregate, boardRoot: root)
#expect(model.plannedRepairs.map(\.repair) == [.mintBoardIndex])
let outcome = BoardRepairRun.apply(model.plannedRepairs, boardRoot: root)
#expect(outcome.failure == nil)
// "(folder-name title, `schema: 1`)" and the extension is not part of the name
// (01 § Board naming).
let loaded = try BoardLoader.load(boardRoot: root)
#expect(loaded.model.title.value == "Roadmap")
#expect(loaded.model.schema == 1)
#expect(loaded.model.lanes.count == 1, "the board that was already there is still there")
}
@Test("The repair bracket's receipts are all heal-marked")
func repairReceiptsAreHealMarked() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", "---\ntitle: No Schema\n---\n")
let model = try makeModel(fixture)
let outcome = BoardRepairRun.apply(model.plannedRepairs, boardRoot: fixture.root)
let index = fixture.root.appendingPathComponent(BoardLoader.indexFileName)
#expect(outcome.ledger.receipt(at: index) != nil, "the write dropped a receipt")
#expect(
outcome.ledger.isHeal(at: index),
"01: repairs drop heal-marked receipts and commit separately as one repair commit"
)
}
@Test("A repair pass with nothing to repair writes nothing and fails nothing")
func anEmptyRepairPassIsANoOp() throws {
let fixture = try makeSkippableBoard()
defer { fixture.tearDown() }
let model = try makeModel(fixture)
model.choose(.skip, inClass: .unreadableFrontmatter)
// Every resolution here is a Skip: Repair and Open is a re-walk and nothing else.
#expect(model.plannedRepairs.isEmpty)
let outcome = BoardRepairRun.apply(model.plannedRepairs, boardRoot: fixture.root)
#expect(outcome.failure == nil)
#expect(outcome.ledger.outstandingReceipts == 0)
}
@Test("A repaired board walks clean, which is the loop Repair and Open runs")
func repairThenWalkIsTheLoop() throws {
let fixture = try makeTwoClassBoard()
defer { fixture.tearDown() }
let model = try makeModel(fixture)
model.choose(.skip, inClass: .unreadableFrontmatter)
BoardRepairRun.apply(model.plannedRepairs, boardRoot: fixture.root)
let result = try BoardLoader.load(boardRoot: fixture.root, skipping: model.skipSet)
#expect(result.model.schema == 1)
// The skipped lane left with its whole subtree; the intact one stayed.
#expect(result.model.lanes.map { $0.id.rawValue } == [Ident.lane1])
#expect(result.warnings.contains(.userSkipped(path: "\(Ident.lane2)/index.md")))
}
@Test("An interrupted batch is accepted: what landed stays, and the rest re-aggregates")
func aPartialRepairReAggregates() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", "---\ntitle: No Schema\n---\n")
// A repair aimed at a file that is not there the shape of a disk that moved underneath.
let planned = [
BoardDecisionSurfaceModel.PlannedRepair(path: "index.md", repair: .stampSchema),
BoardDecisionSurfaceModel.PlannedRepair(
path: "\(Ident.lane1)/index.md", repair: .stampSchema)
]
let outcome = BoardRepairRun.apply(planned, boardRoot: fixture.root)
#expect(outcome.failure != nil, "the second repair could not run")
#expect(try fixture.indexText("").contains("schema: 1"), "the first one still landed")
// "every intermediate state valid, and a partial repair simply re-aggregates on the next walk".
#expect(throws: Never.self) { try BoardLoader.load(boardRoot: fixture.root) }
}
}
// MARK: - The skip channel through the session
@MainActor
@Suite("Decision surface ▸ the skip set rides the session")
struct BoardDecisionSurfaceSkipTests {
@Test("A skip-carrying open builds a store that retains the set")
func theStoreRetainsTheSkipSet() async throws {
let fixture = try makeSkippableBoard()
defer { fixture.tearDown() }
let registry = BoardStoreRegistry()
let skips: Set<String> = ["\(Ident.lane2)/index.md", "\(Ident.lane1)/\(Ident.card2)/index.md"]
let store = try #require(try await registry.acquireOffMain(fixture.root, skipping: skips))
defer { registry.release(store) }
#expect(store.skippedPaths == skips)
#expect(store.snapshot.lanes.map { $0.id.rawValue } == [Ident.lane1])
#expect(store.snapshot.lanes.first?.cards.count == 1, "the newer-schema card left too")
}
@Test("Every reload of that session passes the same set")
func reloadsCarryTheSkipSet() async throws {
let fixture = try makeSkippableBoard()
defer { fixture.tearDown() }
let registry = BoardStoreRegistry()
let skips: Set<String> = ["\(Ident.lane2)/index.md", "\(Ident.lane1)/\(Ident.card2)/index.md"]
let store = try #require(try await registry.acquireOffMain(fixture.root, skipping: skips))
defer { registry.release(store) }
// A foreign filesystem event. Without the retained set this reload walks the still-broken
// board, fails, and the window carries a breakage banner over the board the user just chose
// to open.
store.handleWatcherEvent(.treeChanged(.foreign))
await store.awaitQuiescence()
#expect(store.reloadFailure == nil, "the open's consent stands for the session")
#expect(store.snapshot.lanes.map { $0.id.rawValue } == [Ident.lane1])
}
@Test("The walk marks every skip loudly, and the notice is written from those marks")
func skipsAreLoudlyMarked() throws {
let fixture = try makeSkippableBoard()
defer { fixture.tearDown() }
// Every defect on this board has to be answered before it opens at all a skip omits the item
// it names, never its siblings' defects.
let skips: Set<String> = ["\(Ident.lane1)/\(Ident.card2)/index.md", "\(Ident.lane2)/index.md"]
let result = try BoardLoader.load(boardRoot: fixture.root, skipping: skips)
let skipped = result.warnings.compactMap { warning -> String? in
if case let .userSkipped(path) = warning { path } else { nil }
}
// Walk order, like every other list the aggregate produces.
#expect(skipped == ["\(Ident.lane1)/\(Ident.card2)/index.md", "\(Ident.lane2)/index.md"])
// The row the opened board carries, values and all.
let items = skipped.map { RevealTarget(path: $0, url: fixture.root.appendingPathComponent($0)) }
let center = BannerCenter()
center.postSkippedOnOpen(items)
let loss = try #require(center.losses.first)
#expect(loss.message == "Opened without 2 items — you chose to skip them")
#expect(loss.reveals.map(\.path) == skipped)
// Each reveal points at the file the surface's row named, still on disk and untouched.
#expect(loss.reveals.allSatisfy { FileManager.default.fileExists(atPath: $0.url.path) })
// "each with Reveal in Finder", as a control the strip can render and Tab can reach.
let row = BannerRow.loss(loss)
#expect(row.tone == .warning, "a warning-tone loss row — nothing failed")
#expect(row.controls.map(\.label) == ["Reveal in Finder", "Dismiss"])
}
@Test("A sole skip is named rather than counted")
func aSoleSkipIsNamed() throws {
let center = BannerCenter()
center.postSkippedOnOpen([
RevealTarget(path: "lane/index.md", url: URL(fileURLWithPath: "/b/lane/index.md"))
])
#expect(center.losses.first?.message == "Opened without 'lane/index.md' — you chose to skip it")
}
@Test("Several skips fold to a count, with a reveal for each")
func severalSkipsFold() throws {
let items = [
RevealTarget(path: "a/index.md", url: URL(fileURLWithPath: "/b/a/index.md")),
RevealTarget(path: "b/index.md", url: URL(fileURLWithPath: "/b/b/index.md")),
RevealTarget(path: "c/index.md", url: URL(fileURLWithPath: "/b/c/index.md"))
]
let center = BannerCenter()
center.postSkippedOnOpen(items)
let loss = try #require(center.losses.first)
#expect(loss.message == "Opened without 3 items — you chose to skip them")
// The fold is only safe because "which ones" survives on the row.
#expect(loss.reveals.count == 3)
}
@Test("An open that skipped nothing says nothing")
func noSkipsNoNotice() {
let center = BannerCenter()
center.postSkippedOnOpen([])
#expect(center.losses.isEmpty)
}
@Test("An ordinary loss row still carries no reveal control")
func ordinaryLossRowsAreUnchanged() throws {
let center = BannerCenter()
center.postSkippedFolders(count: 2)
let loss = try #require(center.losses.first)
#expect(loss.reveals.isEmpty)
#expect(BannerRow.loss(loss).controls.map(\.label) == ["Dismiss"])
}
}
// MARK: - The attendance branch
@MainActor
@Suite("Decision surface ▸ the attendance branch")
struct BoardDecisionSurfaceAttendanceTests {
private func aggregate(_ reason: BoardLoadError.Reason, path: String = "index.md") -> BoardLoadFailure {
BoardLoadFailure(BoardLoadError(path: path, reason: reason))
}
@Test("A restored open never reaches the surface, whatever is wrong")
func restoredAlwaysRetires() {
// "restoration failures keep the retire-to-welcome-row landing launch never chains dialogs".
let repairable = aggregate(.missingSchema)
let handEdit = aggregate(.unparseableYAML(message: "x", line: 1), path: "lane/index.md")
#expect(BoardWindowHost.landing(for: repairable, origin: .restored) == .retire)
#expect(BoardWindowHost.landing(for: handEdit, origin: .restored) == .retire)
}
@Test("An attended open reaches the surface for anything a repair could touch")
func attendedDecides() {
#expect(BoardWindowHost.landing(for: aggregate(.missingSchema), origin: .attended) == .decide)
#expect(BoardWindowHost.landing(for: aggregate(.boardRootMissingIndex), origin: .attended) == .decide)
#expect(BoardWindowHost.landing(for: aggregate(.schemaNewerThanApp(found: 9)), origin: .attended) == .decide)
}
@Test("An environmental failure retires even when attended")
func theEnvironmentalCarveOut() {
// Redesign Gap 87cd782a: there is nothing on disk to repair, so a surface would offer a
// decision with no choices in it. Welcome's row says the same thing in one line.
let gone = BoardLoadFailure(
BoardLoadError(path: ".", reason: .unreadableRoot(message: "no such file or directory")))
let file = BoardLoadFailure(BoardLoadError(path: ".", reason: .notADirectory))
#expect(BoardWindowHost.landing(for: gone, origin: .attended) == .retire)
#expect(BoardWindowHost.landing(for: file, origin: .attended) == .retire)
}
@Test("An environmental defect among repairable ones still shows the surface")
func aMixedAggregateStillDecides() {
// The carve-out is "nothing to repair", not "one of these is environmental": the rest of the
// aggregate is still actionable.
let mixed = BoardLoadFailure([
BoardLoadError(path: "index.md", reason: .missingSchema),
BoardLoadError(path: ".", reason: .notADirectory)
])
#expect(BoardWindowHost.landing(for: mixed, origin: .attended) == .decide)
}
@Test("The origin rides the pending open, attended by default")
func theOriginRidesThePendingOpen() throws {
let folder = FileManager.default.temporaryDirectory
.appendingPathComponent("DecisionOrigin-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: folder) }
let model = AppModel(
registryStorageURL: folder.appendingPathComponent("board-registry.json"),
clipboardStagingRoot: folder.appendingPathComponent("Clipboard", isDirectory: true)
)
let attended = folder.appendingPathComponent("Attended.kanban", isDirectory: true)
let restored = folder.appendingPathComponent("Restored.kanban", isDirectory: true)
// The stash `openBoard(at:origin:)` makes reached directly, because that method needs a
// SwiftUI `OpenWindowAction` no test can build (`AppModel.stashPendingOpen`).
model.stashPendingOpen(for: BoardWindowRef(url: attended), url: attended, origin: .attended)
model.stashPendingOpen(for: BoardWindowRef(url: restored), url: restored, origin: .restored)
let attendedClaim = model.claimPendingOpen(for: BoardWindowRef(url: attended))
#expect(attendedClaim.origin == .attended)
#expect(attendedClaim.access != nil, "the scope rides the same stash")
#expect(model.claimPendingOpen(for: BoardWindowRef(url: restored)).origin == .restored)
// Claiming removes it, and a window that arrived by some other route reads attended with no
// scope the safe direction (the worst it can do is offer a repair nobody asked for).
let second = model.claimPendingOpen(for: BoardWindowRef(url: attended))
#expect(second.origin == .attended)
#expect(second.access == nil)
}
}
// MARK: - The Pro repair commit
/// HEAD's first-parent ancestry, newest first read through SwiftGitX, never through the committer
/// that made the commits (`AutoCommitTests`' rule, kept: nothing here shells out to `git`).
private func repairHistory(at boardRoot: URL, limit: Int = 8) throws -> [(subject: String, authorName: String, authorEmail: String, committerName: String)] {
let repository = try Repository.open(at: boardRoot)
guard !repository.isHEADUnborn, let tip = try repository.HEAD.target as? Commit else { return [] }
var records: [(String, String, String, String)] = []
var current: Commit? = tip
while let commit = current, records.count < limit {
records.append((commit.summary, commit.author.name, commit.author.email, commit.committer.name))
current = (try? commit.parents)?.first
}
return records
}
@MainActor
@Suite("Decision surface ▸ the Pro repair commit")
struct BoardDecisionSurfaceRepairCommitTests {
/// An `AppModel` whose app-side state lives in temp rather than in the app's real Application
/// Support home, and which reads as Pro `AutoCommitCompositionRootTests`' fixture.
private func makeModel() throws -> (model: AppModel, tearDown: () -> Void) {
let folder = FileManager.default.temporaryDirectory
.appendingPathComponent("DecisionRepairCommit-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
let model = AppModel(
registryStorageURL: folder.appendingPathComponent("board-registry.json"),
clipboardStagingRoot: folder.appendingPathComponent("Clipboard", isDirectory: true)
)
model.currentTier = { .pro }
return (model, { try? FileManager.default.removeItem(at: folder) })
}
/// **A repaired Pro board's first flush is one heal commit, authored by the integrity identity**
/// (01-storage-format.md § Malformed input: "On Pro boards the repairs drop heal-marked receipts
/// and commit separately as one repair commit, never folded into anyone else's work";
/// 06-history-undo.md Commit messages Healing mutations commit separately).
///
/// The whole chain is exercised end to end, because every link in it can fail silently and the
/// symptom is identical each time a commit blaming the outside world for the app's own repair:
///
/// 1. The surface's default resolution is the minted repair.
/// 2. `BoardRepairRun` writes it store-lessly and marks every receipt in its own ledger.
/// 3. The store built by the following walk **adopts** that ledger (`EchoLedger.adopt`)
/// before `beginSession`, which is where Pro's committer is composed and started.
/// 4. `GitAutoCommitter.start()` harvests, so the debounce it arms can see receipts that were
/// dropped before any write bracket of this session existed.
/// 5. `CommitAttribution.split` sorts the repaired path into the heal class, and the heal class
/// is authored `Lanework Integrity <integrity@lanework.invalid>` with the user as committer.
@Test("A repaired board under Pro + git produces a separate heal commit")
func aRepairCommitsAsTheIntegrityIdentity() async throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
// A board whose root is missing `schema` the minted stamp's own case. The lane is here so
// the repository has an ordinary tree around the file being repaired.
try fixture.item("", "---\ntitle: Needs A Stamp\ncreated: 2026-01-01T09:00:00Z\n---\nBoard.\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Todo\norder: 1024\n---\n")
let (model, tearDown) = try makeModel()
defer { tearDown() }
// The repository, with everything as it stands committed including the broken root, which is
// what makes the repair a real change rather than a fresh file.
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro, ledger: EchoLedger()))
#expect(await git.addGit())
let commitsBefore = try repairHistory(at: fixture.root).count
git.stopAutoCommit()
// The surface, exactly as the window builds it.
let surface = BoardDecisionSurfaceModel(failure: try failure(of: fixture), boardRoot: fixture.root)
#expect(surface.canRepairAndOpen, "a lone missing root schema is a minted repair, preselected")
// Repair and Open's write half.
let outcome = BoardRepairRun.apply(surface.plannedRepairs, boardRoot: fixture.root)
#expect(outcome.failure == nil)
// The walk that follows, and the store it builds. Built directly rather than through the
// registry so this case is about the repair's commit and not about the open's *other* heals
// (the agent guide, the `.gitignore` seed), which the registry's acquire also fires.
let result = try BoardLoader.load(boardRoot: fixture.root, skipping: surface.skipSet)
let store = BoardStore(rootURL: fixture.root, loaded: result, skipping: surface.skipSet)
// The adoption, before the session composes the committer.
store.echoes.adopt(outcome.ledger)
let ref = BoardWindowRef(url: fixture.root)
let recordID = model.boardRegistry.recordOpen(of: fixture.root)
model.beginSession(ref: ref, store: store, recordID: recordID, access: nil)
let committer = try #require(model.session(for: ref)?.git?.committer)
// The debounce `start()` armed is not what this asserts; the explicit flush is.
committer.stop()
committer.debounceInterval = .seconds(30)
committer.coveringSnapshotDeadline = .milliseconds(50)
committer.coveringSnapshotPollInterval = .milliseconds(5)
await committer.flushNow()
let log = try repairHistory(at: fixture.root)
#expect(log.count == commitsBefore + 1, "one repair commit, never folded and never split further")
let head = try #require(log.first)
#expect(head.authorName == CommitAttribution.integrityAuthorName)
#expect(head.authorEmail == CommitAttribution.integrityAuthorEmail)
#expect(
head.authorEmail != CommitAttribution.externalAuthorEmail,
"the app's own repair must never be blamed on the outside world"
)
// "the committer stays the user (the recorded-by convention)".
#expect(head.committerName == GitCommitOperation.userIdentity(at: fixture.root).name)
#expect(GitCommitOperation.changedPaths(at: fixture.root).isEmpty, "the flush leaves nothing dirty")
}
}