import Foundation
import Testing
@testable import Kanban

// MARK: - Fixture builder

/// A tiny in-memory-driven builder for synthetic board trees under a temp directory. Smoke
/// coverage only (per 02-architecture.md § Testing) — a comprehensive golden-fixture suite
/// over real trees under `Fixtures/` is a separate, later card.
private struct BoardFixture {
    let root: URL

    init() throws {
        root = FileManager.default.temporaryDirectory
            .appendingPathComponent("BoardLoaderTests-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
    }

    func tearDown() {
        try? FileManager.default.removeItem(at: root)
    }

    /// Writes `index.md` at `relativePath` (created if needed), with `body` after the
    /// frontmatter delimiters.
    @discardableResult
    func index(_ relativePath: String, _ frontmatter: String, body: String = "") throws -> URL {
        let folder = relativePath.isEmpty ? root : root.appendingPathComponent(relativePath, isDirectory: true)
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
        let text = "---\n\(frontmatter)---\n\(body)"
        try text.write(to: folder.appendingPathComponent("index.md"), atomically: true, encoding: .utf8)
        return folder
    }

    /// Creates a folder with no `index.md` — the "interrupted two-step create" shape.
    @discardableResult
    func emptyFolder(_ relativePath: String) throws -> URL {
        let folder = root.appendingPathComponent(relativePath, isDirectory: true)
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
        return folder
    }

    /// A stray file, not a folder — never a level candidate regardless of its name.
    func strayFile(_ relativePath: String, contents: String = "stray") throws {
        let url = root.appendingPathComponent(relativePath)
        try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
        try contents.write(to: url, atomically: true, encoding: .utf8)
    }
}

/// A fresh folder name in the app's own emission spelling — lowercase v4. The loader's gate is
/// shape-only (hex, `8-4-4-4-12`, any case, any version — 01-storage-format.md § Fractal layout
/// ▸ Rules, "Name shape gates level detection"), so this is *a* valid identity rather than the
/// only kind; the case tests below cover the rest. Used wherever a test just needs some lane/card
/// identity and doesn't care about the exact value; tests that need a specific lexicographic
/// ordering use literal UUID-shaped strings instead.
private func uuidFolderName() -> String {
    UUID().uuidString.lowercased()
}

/// One defect, and **only** one: the whole aggregate is asserted rather than its first entry, so a
/// board authored to break in one place cannot quietly start reporting two.
private func expectFailure(
    _ expectedReason: BoardLoadError.Reason,
    path: String,
    _ operation: () throws -> Void
) {
    do {
        try operation()
        Issue.record("expected BoardLoadError(\(path), \(expectedReason)) but load succeeded")
    } catch let failure as BoardLoadFailure {
        #expect(failure.defects == [BoardLoadError(path: path, reason: expectedReason)])
    } catch {
        Issue.record("expected a BoardLoadFailure, got \(error)")
    }
}

// MARK: - Well-formed board

struct BoardLoaderWellFormedTests {
    @Test func loadsCompleteStructureWithOrderingAndTombstoneFlags() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let laneA = uuidFolderName()
        let laneB = uuidFolderName()
        let cardFirst = uuidFolderName()
        let cardSecond = uuidFolderName()
        let cardDeleted = uuidFolderName()

        try fixture.index("", "schema: 1\ntitle: Demo Board\ntemplate: {order: 3}\n")
        try fixture.index(laneB, "schema: 1\norder: 2048\ntitle: B Lane\n")
        try fixture.index(laneA, "schema: 1\norder: 1024\ntitle: A Lane\n")

        try fixture.index("\(laneA)/\(cardSecond)", "schema: 1\norder: 2048\ntitle: Second\n")
        try fixture.index("\(laneA)/\(cardFirst)", "schema: 1\norder: 1024\ntitle: First\n")
        try fixture.index(
            "\(laneA)/\(cardDeleted)",
            "schema: 1\norder: 512\ntitle: Gone\ndeleted: 2026-01-01T00:00:00Z\n"
        )

        let result = try BoardLoader.load(boardRoot: fixture.root)
        let model = result.model

        #expect(model.title.value == "Demo Board")
        #expect(model.schema == 1)
        if case let .mapping(pairs) = model.template {
            #expect(pairs.count == 1)
        } else {
            Issue.record("expected template to be a mapping, got \(String(describing: model.template))")
        }

        #expect(model.lanes.map(\.id.rawValue) == [laneA, laneB])

        let lane = try #require(model.lanes.first { $0.id.rawValue == laneA })
        #expect(lane.cards.map(\.id.rawValue) == [cardDeleted, cardFirst, cardSecond])
        #expect(lane.cards.map(\.isDeleted) == [true, false, false])
        #expect(result.warnings.isEmpty)
    }

    /// Tombstone semantics key on the `deleted` key's *presence*, not its validity
    /// (01-storage-format.md § Frontmatter, § Deletion): a `deleted` value with no sensible
    /// date reading still tombstones — the user's intent to delete outranks the broken date.
    @Test func malformedDeletedValueStillTombstonesLaneAndCard() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()
        let card = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\ndeleted: yesterday\n")
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ndeleted: yesterday\n")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        let loadedLane = try #require(result.model.lanes.first { $0.id.rawValue == lane })
        #expect(loadedLane.deleted == .malformed(raw: "yesterday"))
        #expect(loadedLane.isDeleted)

        let loadedCard = try #require(loadedLane.cards.first { $0.id.rawValue == card })
        #expect(loadedCard.deleted == .malformed(raw: "yesterday"))
        #expect(loadedCard.isDeleted)
    }

    @Test func tiesAreBrokenByFolderNameNotTitle() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        // Both UUID-shaped so both are lane candidates; chosen so their lexicographic order
        // ('0' < 'f') is known ahead of time.
        let laneFirst = "00000000-0000-4000-8000-000000000000"
        let laneSecond = "ffffffff-ffff-4fff-8fff-ffffffffffff"

        try fixture.index("", "schema: 1\n")
        // Same order; titles are reversed relative to folder-name order, to catch a loader
        // that accidentally wires title into the tie-break instead of the folder name.
        try fixture.index(laneSecond, "schema: 1\norder: 1024\ntitle: Should Be Second\n")
        try fixture.index(laneFirst, "schema: 1\norder: 1024\ntitle: Should Be First\n")

        let model = try BoardLoader.load(boardRoot: fixture.root).model
        #expect(model.lanes.map(\.id.rawValue) == [laneFirst, laneSecond])
    }
}

// MARK: - Skip rules (UUID-shaped candidates only)

struct BoardLoaderSkipTests {
    @Test func indexlessUUIDFolderBelowRootIsSkippedWithMissingIndexWarning() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()
        let orphanLane = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.emptyFolder(orphanLane)

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.map(\.id.rawValue) == [lane])
        #expect(result.warnings.contains(.missingIndex(path: orphanLane)))
    }

    @Test func indexlessUUIDCardFolderIsSkippedWithWarningAndRestOfBoardStillLoads() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()
        let card = uuidFolderName()
        let orphanCard = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\n")
        try fixture.emptyFolder("\(lane)/\(orphanCard)")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        let loadedLane = try #require(result.model.lanes.first)
        #expect(loadedLane.cards.map(\.id.rawValue) == [card])
        #expect(result.warnings.contains(.missingIndex(path: "\(lane)/\(orphanCard)")))
    }
}

// MARK: - Strays ignored (non-directories, hidden entries, symlinks)

struct BoardLoaderStrayTests {
    @Test func strayFilesAndHiddenEntriesAreIgnoredWithoutWarning() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()
        let card = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\n")

        try fixture.strayFile("notes.txt")
        try fixture.strayFile(".DS_Store")
        try fixture.strayFile("\(lane)/notes.txt")
        _ = try fixture.emptyFolder(".git")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.map(\.id.rawValue) == [lane])
        #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [card])
        #expect(result.warnings.isEmpty)
    }

    /// **`.backgrounds/` is invisible to the loader** (03-board-ui.md § Styling ▸ Capabilities; ruled
    /// 2026-08-09) — no code change earns this: it is a hidden, non-UUID-shaped name, exactly the
    /// stray shape `.trash` and `.DS_Store` already exercise above. Its contents (a generated PNG, in
    /// this case) never appear as a lane candidate and never draw a `nonUUIDFolderIgnored` warning,
    /// because the directory walk that would notice never descends into it at all.
    @Test func backgroundsFolderAndItsContentsAreIgnoredWithoutWarning() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.strayFile(".backgrounds/facets.png", contents: "not really a png")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.map(\.id.rawValue) == [lane])
        #expect(result.warnings.isEmpty)
    }

    @Test func directorySymlinkIsTreatedAsStrayNotFollowed() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let realLane = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        let realLaneURL = try fixture.index(realLane, "schema: 1\norder: 1024\n")
        try FileManager.default.createSymbolicLink(
            at: fixture.root.appendingPathComponent("linked-lane"),
            withDestinationURL: realLaneURL
        )

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.map(\.id.rawValue) == [realLane])
        #expect(result.warnings.isEmpty)
    }

    /// The identity predicate is shape-only — "identity-shaped name or not" (01-storage-format.md
    /// § Fractal layout ▸ Rules, "Symlinks are never traversed", settled). This pins the case the
    /// previous test's non-UUID name doesn't: a symlink whose *name itself* would pass
    /// `isUUIDShaped` and which points at a real directory must still be excluded before that
    /// name shape is ever consulted — `directoryCandidates` filters on `isSymbolicLink` ahead of
    /// `isDirectory`, so a link is never mistaken for the directory it points to, UUID-shaped name
    /// or not.
    @Test func uuidShapedSymlinkToADirectoryIsTreatedAsStrayNotFollowed() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let realLane = uuidFolderName()
        let linkedName = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        let realLaneURL = try fixture.index(realLane, "schema: 1\norder: 1024\n")
        try FileManager.default.createSymbolicLink(
            at: fixture.root.appendingPathComponent(linkedName),
            withDestinationURL: realLaneURL
        )

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.map(\.id.rawValue) == [realLane])
        #expect(!result.model.lanes.map(\.id.rawValue).contains(linkedName))
        #expect(result.warnings.isEmpty)
    }
}

// MARK: - Non-UUID-shaped folders are strays (01-storage-format.md § Fractal layout ▸ Rules,
// "Name shape gates level detection")

struct BoardLoaderNonUUIDStrayTests {
    /// A non-UUID-shaped folder is a stray even when its `index.md` is perfectly valid
    /// lane-shaped content — the name shape gates candidacy before the file is ever read.
    @Test func nonUUIDFolderWithValidLaneShapedIndexIsIgnoredWithWarningAndBoardLoads() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let realLane = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(realLane, "schema: 1\norder: 1024\n")
        try fixture.index("todo", "schema: 1\norder: 2048\ntitle: Hand-authored lane\n")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.map(\.id.rawValue) == [realLane])
        #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "todo")))
    }

    /// The motivating case: before this rule, a non-UUID folder with a broken `index.md`
    /// (missing required `order`) would fail-fast the whole load. Now the name shape gates it
    /// out as a stray before the loader ever parses the file, so the rest of the board still
    /// loads.
    @Test func nonUUIDFolderWithBrokenIndexIsIgnoredWithWarningAndBoardStillLoads() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let realLane = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(realLane, "schema: 1\norder: 1024\n")
        // A hand-authored lane with a name that isn't identity-shaped: never a candidate, so its
        // contents are never read at all.
        try fixture.index("todo", "schema: 1\ntitle: Broken hand-authored lane\n")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.map(\.id.rawValue) == [realLane])
        #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "todo")))
    }

    /// Same motivating case, one level down: a non-UUID card-depth folder with a broken
    /// `index.md` is a stray, not a fail-fast, and doesn't stop its lane's other cards loading.
    @Test func nonUUIDCardFolderWithBrokenIndexIsIgnoredWithWarningAndLaneStillLoads() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()
        let realCard = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(realCard)", "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/scratch", "schema: 1\n") // missing 'order' too

        let result = try BoardLoader.load(boardRoot: fixture.root)
        let loadedLane = try #require(result.model.lanes.first)
        #expect(loadedLane.cards.map(\.id.rawValue) == [realCard])
        #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "\(lane)/scratch")))
    }

    /// The identity predicate is **shape-only, any case** (01-storage-format.md § Fractal layout
    /// ▸ Rules, settled): `uuidgen` and `UUID().uuidString` both print uppercase, so an
    /// uppercase folder is an ordinary lane — never a silently skipped stray. Its `rawValue`
    /// keeps the exact spelling: the app accepts liberally and never renames to canonicalize.
    @Test func uppercaseUUIDFolderIsALaneWithItsSpellingPreserved() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lowercaseLane = uuidFolderName()
        let uppercaseLane = UUID().uuidString // Foundation renders this uppercase.

        try fixture.index("", "schema: 1\n")
        try fixture.index(lowercaseLane, "schema: 1\norder: 1024\n")
        try fixture.index(uppercaseLane, "schema: 1\norder: 2048\n")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.map(\.id.rawValue) == [lowercaseLane, uppercaseLane])
        #expect(result.warnings.isEmpty)
    }

    /// One level down, and mixed case rather than uniform: an agent's `uuidgen`-named card
    /// folder loads as a card, spelling intact.
    @Test func mixedCaseUUIDCardFolderLoadsAsACardWithItsSpellingPreserved() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()
        let mixedCaseCard = "AbCdEf01-2345-6789-aBcD-EF0123456789"

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(mixedCaseCard)", "schema: 1\norder: 1024\ntitle: From an agent\n")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        let loadedLane = try #require(result.model.lanes.first)
        #expect(loadedLane.cards.map(\.id.rawValue) == [mixedCaseCard])
        #expect(loadedLane.cards.first?.title.value == "From an agent")
        #expect(result.warnings.isEmpty)
    }

    /// **Any version**, not just v4: the version nibble protects no invariant here — a v7 (or a
    /// nibble no RFC ever assigned) is exactly as unique as a v4, so it is an identity, not a
    /// stray. Only the shape is checked.
    @Test func oddVersionAndVariantNibblesAreStillIdentities() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let v7Lane = "01912d5e-7c00-7000-8000-abcdefabcdef"    // version nibble 7
        let oddLane = "01912d5e-7c00-c000-f000-abcdefabcdef"   // version c, variant f — no RFC's

        try fixture.index("", "schema: 1\n")
        try fixture.index(v7Lane, "schema: 1\norder: 1024\n")
        try fixture.index(oddLane, "schema: 1\norder: 2048\n")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.map(\.id.rawValue) == [v7Lane, oddLane])
        #expect(result.warnings.isEmpty)
    }

    /// The shape itself is still strict: near-misses stay strays. Hex only, exact group lengths,
    /// hyphens exactly where they belong.
    @Test func nearMissUUIDShapesAreStillStrays() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let realLane = uuidFolderName()
        let strays = [
            "GGGGGGGG-0000-4000-8000-000000000000",   // not hex
            "00000000-0000-4000-8000-00000000000",    // one digit short
            "000000000-000-4000-8000-000000000000",   // hyphens misplaced
            "00000000-0000-4000-8000-000000000000-",  // trailing hyphen
        ]

        try fixture.index("", "schema: 1\n")
        try fixture.index(realLane, "schema: 1\norder: 1024\n")
        for (offset, stray) in strays.enumerated() {
            try fixture.index(stray, "schema: 1\norder: \(2048 + offset)\n")
        }

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.map(\.id.rawValue) == [realLane])
        for stray in strays {
            #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: stray)))
        }
    }

    /// Reserved card children are covered "by construction": `attachments/` and `comments/` are
    /// non-UUID-shaped, and the *level walk* stops at depth 2 — so neither can ever be mistaken
    /// for an item, and neither may surface a warning. (`attachments/` is read for its file
    /// names, which is a listing, not a descent — see `CardAttachmentListingTests` below.)
    @Test func reservedAttachmentsAndCommentsUnderCardProduceNoWarning() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()
        let card = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index("\(lane)", "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\n")
        try fixture.strayFile("\(lane)/\(card)/attachments/sketch.png")
        try fixture.strayFile("\(lane)/\(card)/comments/whatever.md")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.first?.cards.map(\.id.rawValue) == [card])
        #expect(result.warnings.isEmpty)
    }
}

// MARK: - Card attachments (01-storage-format.md § Attachments)

/// `Card.attachments` — the loader's one read *inside* a card folder. The golden-fixture suite
/// pins the everyday shapes on real committed trees (`FixtureBoardTests`); these cover what a git
/// fixture can't carry (a symlink) and what only a synthetic tree can arrange (a card folder whose
/// `attachments` is a *file*, an empty folder, Finder's numeric ordering).
struct CardAttachmentListingTests {

    /// Builds a one-card board and returns that card, so each test below is one arrangement plus
    /// one assertion.
    private func card(in fixture: BoardFixture) throws -> Card {
        let result = try BoardLoader.load(boardRoot: fixture.root)
        return try #require(result.model.lanes.first?.cards.first)
    }

    private func boardWithOneCard(_ fixture: BoardFixture) throws -> String {
        let lane = "10000000-0000-4000-8000-000000000001"
        let cardID = "20000000-0000-4000-8000-000000000002"
        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(cardID)", "schema: 1\norder: 1024\n")
        return "\(lane)/\(cardID)"
    }

    /// **Symlinks are not surfaced** — the same never-resolve stance the level walk takes
    /// (`directoryCandidates`), so a link into another volume or a cycle can't turn a listing
    /// into a traversal. The link itself stays on disk untouched; it just isn't an attachment.
    @Test func symlinksInAttachmentsAreNotSurfaced() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)

        try fixture.strayFile("\(cardPath)/attachments/real.png")
        try fixture.strayFile("outside.png")
        let attachments = fixture.root.appendingPathComponent("\(cardPath)/attachments", isDirectory: true)
        try FileManager.default.createSymbolicLink(
            at: attachments.appendingPathComponent("link-to-file.png"),
            withDestinationURL: fixture.root.appendingPathComponent("outside.png")
        )
        try FileManager.default.createSymbolicLink(
            at: attachments.appendingPathComponent("link-to-folder"),
            withDestinationURL: fixture.root
        )

        #expect(try card(in: fixture).attachments == ["real.png"])
    }

    /// The four shapes in one folder — the fixture board's assertion restated synthetically, so
    /// the rule is pinned even if the bundled fixture tree ever loses a file to a copy phase.
    @Test func onlyTopLevelNonHiddenRegularFilesAreListed() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)

        try fixture.strayFile("\(cardPath)/attachments/sketch.png")
        try fixture.strayFile("\(cardPath)/attachments/notes.txt")
        try fixture.strayFile("\(cardPath)/attachments/.DS_Store")
        try fixture.strayFile("\(cardPath)/attachments/sub/nested.txt")

        #expect(try card(in: fixture).attachments == ["notes.txt", "sketch.png"])
    }

    /// **Finder order** (`localizedStandardCompare`), not plain lexicographic — digits inside a
    /// name *count* rather than collate, so the run `importAttachments` itself produces on a
    /// collision (`shot.png` → `shot 2.png` → `shot 10.png`) sorts 2-before-10, matching the card
    /// window sidebar's listing, which answers through this same enumeration. (`shot.png` trailing
    /// its own numbered copies is Finder's own ordering of a space against a dot, not a quirk of
    /// ours.)
    @Test func namesSortInFinderOrderSoNumbersCountRatherThanCollate() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)

        for name in ["shot 10.png", "shot 2.png", "shot.png", "page-10.txt", "page-2.txt"] {
            try fixture.strayFile("\(cardPath)/attachments/\(name)")
        }

        #expect(try card(in: fixture).attachments == [
            "page-2.txt", "page-10.txt", "shot 2.png", "shot 10.png", "shot.png",
        ])
    }

    @Test func anEmptyAttachmentsFolderListsNothing() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)
        try fixture.emptyFolder("\(cardPath)/attachments")

        #expect(try card(in: fixture).attachments.isEmpty)
    }

    /// A listing that *cannot* be made degrades to `[]` — here because a hand-editor left a
    /// `attachments` **file** where the folder would be. Fail-fast is reserved for structure
    /// (01-storage-format.md § Malformed input); a cosmetic field must never be why a board
    /// refuses to open, and the file itself is preserved verbatim like any other stray.
    @Test func anAttachmentsThatIsNotADirectoryDegradesToAnEmptyListing() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)
        try fixture.strayFile("\(cardPath)/attachments", contents: "not a folder")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.first?.cards.first?.attachments.isEmpty == true)
        #expect(result.warnings.isEmpty)
    }

    /// `BoardWriter.listAttachments` — the card window sidebar's authoritative listing — and
    /// `Card.attachments` are **one enumeration**, so a sidebar and a face looking at the same
    /// card can never disagree about its files or their order.
    @Test func theSnapshotsListingAndTheWritersAreTheSameAnswer() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)

        for name in ["shot 10.png", "shot 2.png", "shot.png", ".hidden"] {
            try fixture.strayFile("\(cardPath)/attachments/\(name)")
        }
        try fixture.strayFile("\(cardPath)/attachments/sub/nested.txt")

        let cardFolder = fixture.root.appendingPathComponent(cardPath, isDirectory: true)
        #expect(try card(in: fixture).attachments == BoardWriter.listAttachments(ofCard: cardFolder))
    }
}

// MARK: - Card comment counts (design ruling 2026-08-09, card e729e30a)

/// `Card.commentCount` — the loader's cheap readdir over `comments/`, `CardAttachmentListingTests`'
/// shape one folder over: what a synthetic tree can arrange that isn't worth a golden fixture (a
/// non-identity-shaped stray, a comment folder missing its `index.md`, the two dot-named
/// exclusions, and the one documented divergence from `CommentThread.load`'s parsed count).
struct CardCommentCountListingTests {

    /// `CardAttachmentListingTests.card(in:)`'s exact twin.
    private func card(in fixture: BoardFixture) throws -> Card {
        let result = try BoardLoader.load(boardRoot: fixture.root)
        return try #require(result.model.lanes.first?.cards.first)
    }

    private func boardWithOneCard(_ fixture: BoardFixture) throws -> String {
        let lane = "10000000-0000-4000-8000-000000000001"
        let cardID = "20000000-0000-4000-8000-000000000002"
        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(cardID)", "schema: 1\norder: 1024\n")
        return "\(lane)/\(cardID)"
    }

    @Test func aCardWithNoCommentsFolderCountsZero() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        _ = try boardWithOneCard(fixture)

        #expect(try card(in: fixture).commentCount == 0)
    }

    @Test func anEmptyCommentsFolderCountsZero() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)
        try fixture.emptyFolder("\(cardPath)/comments")

        #expect(try card(in: fixture).commentCount == 0)
    }

    @Test func identityShapedFoldersWithIndexAreCounted() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)

        try fixture.index("\(cardPath)/comments/\(uuidFolderName())", "schema: 1\n")
        try fixture.index("\(cardPath)/comments/\(uuidFolderName())", "schema: 1\n")
        try fixture.index("\(cardPath)/comments/\(uuidFolderName())", "schema: 1\n")

        #expect(try card(in: fixture).commentCount == 3)
    }

    /// A hand-made folder — `notes`, say — under `comments/` is a stray to `CommentThread.load`
    /// (`.notIdentityShaped`), and is excluded here for the identical reason: only a name with a
    /// UUID's shape is a comment candidate at all (`identityShapedChildren`, shared with the
    /// trash entry's own held-card count).
    @Test func nonIdentityShapedFoldersAreExcluded() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)

        try fixture.index("\(cardPath)/comments/\(uuidFolderName())", "schema: 1\n")
        try fixture.index("\(cardPath)/comments/notes", "schema: 1\n")

        #expect(try card(in: fixture).commentCount == 1)
    }

    /// The two-step-create tolerance, one level down: a UUID-shaped folder with no `index.md` yet
    /// (`.missingIndex` to `CommentThread.load`) is not a comment to count either.
    @Test func identityShapedFoldersMissingIndexAreExcluded() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)

        try fixture.index("\(cardPath)/comments/\(uuidFolderName())", "schema: 1\n")
        try fixture.emptyFolder("\(cardPath)/comments/\(uuidFolderName())")

        #expect(try card(in: fixture).commentCount == 1)
    }

    /// `.draft` and `.trash/` are excluded **for free** — both dot-prefixed, and the loader's
    /// directory listing skips hidden entries, exactly the exclusion `CommentThread.load`
    /// documents for the same two folders. A trashed comment sitting inside `.trash/` must not
    /// inflate the count even though it is itself identity-shaped with a readable `index.md`.
    @Test func draftAndTrashAreExcludedForFree() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)

        try fixture.index("\(cardPath)/comments/\(uuidFolderName())", "schema: 1\n")
        try fixture.index("\(cardPath)/comments/.draft", "schema: 1\n")
        try fixture.index("\(cardPath)/comments/.trash/\(uuidFolderName())", "schema: 1\n")

        #expect(try card(in: fixture).commentCount == 1)
    }

    /// **The loader's count and `CommentThread.load`'s parsed count agree** in the well-formed
    /// case — the design ruling's "read how the comments pane counts and match it exactly" (card
    /// e729e30a), pinned the way `theSnapshotsListingAndTheWritersAreTheSameAnswer` pins the
    /// attachment listing's agreement one type over.
    @Test func theLoadersCountAgreesWithTheThreadsParsedCount() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)

        for _ in 0..<4 {
            try fixture.index("\(cardPath)/comments/\(uuidFolderName())", "schema: 1\n")
        }

        let cardFolder = fixture.root.appendingPathComponent(cardPath, isDirectory: true)
        let thread = CommentThread.load(inCard: cardFolder, path: cardPath)
        #expect(thread.comments.count == 4)
        #expect(try card(in: fixture).commentCount == thread.comments.count)
    }

    /// **The one documented divergence**: a comment whose `index.md` exists but fails to parse is
    /// a `Stray` to the thread read (`.unreadable`) — excluded from the pane's count — but the
    /// loader's readdir only checks that `index.md` exists, not that it parses, so the face may
    /// read one higher than the pane until the folder is fixed or removed (`Card.commentCount`'s
    /// own doc comment; `BoardLoader.commentCount(in:)`'s).
    @Test func aMalformedCommentIndexIsCountedByTheLoaderButNotByTheThread() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let cardPath = try boardWithOneCard(fixture)
        let commentID = uuidFolderName()
        // Unparseable YAML — `CommentThreadTests.brokenCommentsAreStrays`' exact shape.
        try fixture.strayFile(
            "\(cardPath)/comments/\(commentID)/index.md", contents: "---\nschema: 1\n  bad: [\n---\nbody\n"
        )

        let cardFolder = fixture.root.appendingPathComponent(cardPath, isDirectory: true)
        let thread = CommentThread.load(inCard: cardFolder, path: cardPath)
        #expect(thread.comments.isEmpty)
        #expect(thread.strays.count == 1)
        #expect(try card(in: fixture).commentCount == 1)
    }
}

// MARK: - ItemID value semantics (01-storage-format.md § Fractal layout ▸ Rules, "Identity
// comparison is UUID-value equality, never string equality")

/// `ItemID` stores the folder's exact spelling — it builds URLs — but *compares* as a UUID
/// value: two case-spellings of one UUID are one identity everywhere.
struct ItemIDValueSemanticsTests {
    private static let lower = "abcdef01-2345-6789-abcd-ef0123456789"
    private static let upper = "ABCDEF01-2345-6789-ABCD-EF0123456789"
    private static let mixed = "AbCdEf01-2345-6789-aBcD-eF0123456789"

    @Test func caseSpellingsOfOneUUIDAreEqualAndHashAlike() {
        let lower = ItemID(rawValue: Self.lower)
        let upper = ItemID(rawValue: Self.upper)
        let mixed = ItemID(rawValue: Self.mixed)

        #expect(lower == upper)
        #expect(lower == mixed)
        #expect(upper == mixed)
        #expect(lower.hashValue == upper.hashValue)
        #expect(upper.hashValue == mixed.hashValue)
    }

    /// Equality is by value, but the spelling is never rewritten — the app accepts liberally and
    /// emits conservatively, and `rawValue` is what builds the folder's URL.
    @Test func rawValueKeepsTheExactSpelling() {
        #expect(ItemID(rawValue: Self.upper).rawValue == Self.upper)
        #expect(ItemID(rawValue: Self.mixed).description == Self.mixed)
    }

    @Test func distinctUUIDsAreUnequalHoweverTheyAreSpelled() {
        let one = ItemID(rawValue: "abcdef01-2345-6789-abcd-ef0123456789")
        let other = ItemID(rawValue: "ABCDEF01-2345-6789-ABCD-EF012345678A")
        #expect(one != other)
    }

    /// The consequence every `Set`/`Dictionary` keyed by `ItemID` inherits — selection
    /// membership included (`ItemReferenceSet`).
    @Test func aSetCollapsesTheTwoSpellingsToOneMember() {
        let set: Set<ItemID> = [ItemID(rawValue: Self.lower), ItemID(rawValue: Self.upper)]
        #expect(set.count == 1)
        #expect(set.contains(ItemID(rawValue: Self.mixed)))

        var byID: [ItemID: String] = [:]
        byID[ItemID(rawValue: Self.upper)] = "written uppercase"
        #expect(byID[ItemID(rawValue: Self.lower)] == "written uppercase")
    }
}

// MARK: - Board-level deleted

struct BoardLoaderBoardLevelDeletedTests {
    @Test func boardLevelDeletedIsIgnoredAndWarned() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        try fixture.index("", "schema: 1\ndeleted: 2026-01-01T00:00:00Z\n")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes.isEmpty)
        #expect(result.warnings.contains(.boardLevelDeletedIgnored))
    }
}

// MARK: - Fail-fast

struct BoardLoaderFailFastTests {
    @Test func rootMissingIndexThrows() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        expectFailure(.boardRootMissingIndex, path: "index.md") {
            _ = try BoardLoader.load(boardRoot: fixture.root)
        }
    }

    @Test func rootThatIsAFileThrowsNotADirectory() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let fileRoot = fixture.root.appendingPathComponent("not-a-folder")
        try "hello".write(to: fileRoot, atomically: true, encoding: .utf8)

        expectFailure(.notADirectory, path: ".") {
            _ = try BoardLoader.load(boardRoot: fileRoot)
        }
    }

    @Test func unreadableRootThrows() throws {
        let missing = FileManager.default.temporaryDirectory
            .appendingPathComponent("BoardLoaderTests-does-not-exist-\(UUID().uuidString)")

        do {
            _ = try BoardLoader.load(boardRoot: missing)
            Issue.record("expected a BoardLoadFailure but load succeeded")
        } catch {
            // Environmental, so a single-defect aggregate: there is no walk behind an unreadable
            // root, and nothing for a second defect to come from.
            #expect(error.defects.count == 1)
            #expect(error.primary.path == ".")
            if case .unreadableRoot = error.primary.reason {
                // expected
            } else {
                Issue.record("expected .unreadableRoot, got \(error.primary.reason)")
            }
        }
    }

    @Test func unparseableYAMLThrows() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        // No closing delimiter.
        try "---\nschema: 1\n".write(
            to: fixture.root.appendingPathComponent("index.md"),
            atomically: true,
            encoding: .utf8
        )

        do {
            _ = try BoardLoader.load(boardRoot: fixture.root)
            Issue.record("expected a BoardLoadFailure but load succeeded")
        } catch {
            #expect(error.primary.path == "index.md")
            if case .unparseableYAML = error.primary.reason {
                // expected
            } else {
                Issue.record("expected .unparseableYAML, got \(error.primary.reason)")
            }
        }
    }

    @Test func missingSchemaThrows() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        try fixture.index("", "title: No Schema\n")

        expectFailure(.missingSchema, path: "index.md") {
            _ = try BoardLoader.load(boardRoot: fixture.root)
        }
    }

    @Test func malformedSchemaThrows() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        try fixture.index("", "schema: not-a-number\n")

        expectFailure(.malformedSchema(raw: "not-a-number"), path: "index.md") {
            _ = try BoardLoader.load(boardRoot: fixture.root)
        }
    }

    @Test func schemaNewerThanAppThrows() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        try fixture.index("", "schema: 2\n")

        expectFailure(.schemaNewerThanApp(found: 2), path: "index.md") {
            _ = try BoardLoader.load(boardRoot: fixture.root)
        }
    }

    /// A schema newer than the app fails fast **below** the root too — the one `schema` rule the
    /// optional-key ruling left alone (01-storage-format.md § Malformed input, re-ruled 2026-07-31).
    @Test func schemaNewerThanAppOnALaneThrows() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 2\norder: 1024\n")

        expectFailure(.schemaNewerThanApp(found: 2), path: "\(lane)/index.md") {
            _ = try BoardLoader.load(boardRoot: fixture.root)
        }
    }

    /// A malformed `schema` below the root still refuses: the ruling made the *absent* key optional,
    /// not the unreadable one — reading `schema: one` as 1 would be inventing agreement.
    @Test func malformedSchemaOnACardThrows() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()
        let card = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(card)", "schema: one\norder: 1024\n")

        expectFailure(.malformedSchema(raw: "one"), path: "\(lane)/\(card)/index.md") {
            _ = try BoardLoader.load(boardRoot: fixture.root)
        }
    }
}

// MARK: - Collect-all, and the skip channel

/// **"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), and its other half: "Skip is
/// user-consented tolerance … per-open decisions, never persisted".
///
/// `Fixtures/Malformed/many-defects.kanban` and `skippable-defects.kanban` are the disk-backed golden
/// boards for both; these are the edges a fixture cannot hold — the trash container (no fixture board
/// carries a `.trash/`) and the environmental failures.
@Suite("BoardLoader ▸ collect-all and skip")
struct BoardLoaderCollectAndSkipTests {

    /// `.trash/` is the walk's last container, so its defects land last — the ordering claim stated
    /// where a fixture cannot state it.
    @Test("Trash defects collect after the lanes, in walk order")
    func trashDefectsCollectLast() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = "10000000-0000-4000-8000-000000000001"
        let card = "20000000-0000-4000-8000-000000000002"
        let entry = "30000000-0000-4000-8000-000000000003"

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(card)", "schema: 9\norder: 1024\n")
        try fixture.index(".trash/\(entry)", "schema: 7\nkind: card\n")

        do {
            _ = try BoardLoader.load(boardRoot: fixture.root)
            Issue.record("a board with two fail-fast defects loaded")
        } catch {
            #expect(error.defects == [
                BoardLoadError(path: "\(lane)/\(card)/index.md", reason: .schemaNewerThanApp(found: 9)),
                BoardLoadError(path: ".trash/\(entry)/index.md", reason: .schemaNewerThanApp(found: 7)),
            ])
        }
    }

    /// A trash entry is skippable like anything else below the root, and skipping it takes it out of
    /// the container rather than out of the board.
    @Test("A skipped trash entry leaves the trash and the board loads")
    func aSkippedTrashEntryLeavesTheTrash() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = "10000000-0000-4000-8000-000000000001"
        let kept = "20000000-0000-4000-8000-000000000002"
        let broken = "30000000-0000-4000-8000-000000000003"

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index(".trash/\(kept)", "schema: 1\nkind: card\ntitle: Kept\n")
        try fixture.index(".trash/\(broken)", "schema: 7\nkind: card\ntitle: Too New\n")

        let result = try BoardLoader.load(
            boardRoot: fixture.root, skipping: [".trash/\(broken)/index.md"])

        #expect(result.model.trash.map(\.id.rawValue) == [kept])
        #expect(result.warnings == [.userSkipped(path: ".trash/\(broken)/index.md")])
    }

    /// **Environmental failures stay immediate**: there is no walk behind a root that is a file, so
    /// the aggregate has exactly one defect and no board was ever read.
    @Test("An environmental failure is a single-defect aggregate")
    func environmentalFailuresAreSingleDefect() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let fileRoot = fixture.root.appendingPathComponent("not-a-folder")
        try "hello".write(to: fileRoot, atomically: true, encoding: .utf8)

        do {
            _ = try BoardLoader.load(boardRoot: fileRoot)
            Issue.record("a file loaded as a board")
        } catch {
            #expect(error.defects == [BoardLoadError(path: ".", reason: .notADirectory)])
            // And the environmental path is unskippable too — a skip set naming it changes nothing.
        }

        do {
            _ = try BoardLoader.load(boardRoot: fileRoot, skipping: ["."])
            Issue.record("a skip set talked the loader into loading a file as a board")
        } catch {
            #expect(error.defects == [BoardLoadError(path: ".", reason: .notADirectory)])
        }
    }

    /// **A skip is per-open and nothing else**: the same loader call without the set refuses again,
    /// which is the ruling's "the next open of a still-broken board presents the surface again"
    /// stated as an assertion. Nothing is written, so nothing can remember.
    @Test("A skip persists nowhere — the next walk refuses again")
    func skipsArePerOpen() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = "10000000-0000-4000-8000-000000000001"
        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 2\norder: 1024\n")

        let skipped = try BoardLoader.load(boardRoot: fixture.root, skipping: ["\(lane)/index.md"])
        #expect(skipped.model.lanes.isEmpty)

        expectFailure(.schemaNewerThanApp(found: 2), path: "\(lane)/index.md") {
            _ = try BoardLoader.load(boardRoot: fixture.root)
        }
    }

    /// The root's four defect shapes are all collected — including the two the design's class list
    /// does not name (`malformedSchema` at the root, and its below-root twin, covered above) — and
    /// the walk still reports what it found underneath.
    @Test("A malformed root schema is collected, and the walk continues under it")
    func aMalformedRootSchemaStillWalks() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = "10000000-0000-4000-8000-000000000001"
        try fixture.index("", "schema: one\n")
        try fixture.index(lane, "schema: 4\norder: 1024\n")

        do {
            _ = try BoardLoader.load(boardRoot: fixture.root)
            Issue.record("a board with a malformed root schema loaded")
        } catch {
            #expect(error.defects == [
                BoardLoadError(path: "index.md", reason: .malformedSchema(raw: "one")),
                BoardLoadError(path: "\(lane)/index.md", reason: .schemaNewerThanApp(found: 4)),
            ])
        }
    }

    /// A root with no `index.md` at all does not end the walk either: the lanes below it are found by
    /// folder shape, so the surface can state the root's minted repair *and* what else is wrong in
    /// the same pass.
    @Test("A missing root index does not end the walk")
    func aMissingRootIndexDoesNotEndTheWalk() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = "10000000-0000-4000-8000-000000000001"
        try fixture.index(lane, "schema: 3\norder: 1024\n")

        do {
            _ = try BoardLoader.load(boardRoot: fixture.root)
            Issue.record("a board with no root index loaded")
        } catch {
            #expect(error.defects == [
                BoardLoadError(path: "index.md", reason: .boardRootMissingIndex),
                BoardLoadError(path: "\(lane)/index.md", reason: .schemaNewerThanApp(found: 3)),
            ])
        }
    }
}

// MARK: - `order` and `schema` optional below the board root

/// **The append-at-end reading** (01-storage-format.md § Ordering, re-ruled 2026-07-31): below the
/// board root a missing, null, non-numeric or non-finite `order` is no longer a fail-fast — it reads
/// as a rank past every ordered sibling, tie-broken by folder name, and the reading is coerce-tier
/// (logged, bytes preserved). `Fixtures/Valid/optional-keys.kanban` is the disk-backed golden case;
/// these are the synthetic edges.
struct BoardLoaderOptionalOrderTests {

    /// The zero-read minimum the ruling exists for: a lane with one ranked card and one card whose
    /// whole frontmatter is a title.
    @Test func orderlessCardAppendsAfterEveryRankedSibling() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = "10000000-0000-4000-8000-000000000001"
        let ranked = "20000000-0000-4000-8000-000000000001"
        // Deliberately the *lower* folder name, so folder order alone would put it first.
        let orderless = "10000000-0000-4000-8000-000000000009"

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(ranked)", "schema: 1\norder: 4096\n")
        try fixture.index("\(lane)/\(orderless)", "title: Minimum\n")

        let model = try BoardLoader.load(boardRoot: fixture.root).model
        #expect(model.lanes[0].cards.map(\.id.rawValue) == [ranked, orderless])
        #expect(model.lanes[0].cards.map(\.order) == [4096, 5120])
    }

    /// Two order-less siblings: folder name decides, and the ranks they read as are `append`'s own
    /// ladder — which is what lets the Writer stamp them without anything moving.
    @Test func twoOrderlessSiblingsSortByFolderName() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = "10000000-0000-4000-8000-000000000001"
        let second = "30000000-0000-4000-8000-000000000002"
        let first = "20000000-0000-4000-8000-000000000001"

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(second)", "schema: 1\n")
        try fixture.index("\(lane)/\(first)", "schema: 1\n")

        let model = try BoardLoader.load(boardRoot: fixture.root).model
        #expect(model.lanes[0].cards.map(\.id.rawValue) == [first, second])
        // No ranked sibling at all, so the ladder bases at 0 — the empty-container convention.
        #expect(model.lanes[0].cards.map(\.order) == [1024, 2048])
    }

    /// The four unusable shapes are one reading. Each is a coercion carrying the text as written.
    @Test func everyUnusableOrderShapeReadsAsAppendAtEnd() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = "10000000-0000-4000-8000-000000000001"
        let anchor = "20000000-0000-4000-8000-000000000000"
        let shapes: [(id: String, frontmatter: String, raw: String)] = [
            ("20000000-0000-4000-8000-000000000001", "schema: 1\n", ""),
            ("20000000-0000-4000-8000-000000000002", "schema: 1\norder:\n", ""),
            ("20000000-0000-4000-8000-000000000003", "schema: 1\norder: null\n", "null"),
            ("20000000-0000-4000-8000-000000000004", "schema: 1\norder: banana\n", "banana"),
            ("20000000-0000-4000-8000-000000000005", "schema: 1\norder: .nan\n", ".nan"),
            ("20000000-0000-4000-8000-000000000006", "schema: 1\norder: .inf\n", ".inf"),
        ]

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(anchor)", "schema: 1\norder: 2048\n")
        for shape in shapes {
            try fixture.index("\(lane)/\(shape.id)", shape.frontmatter)
        }

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [anchor] + shapes.map(\.id))
        #expect(result.model.lanes[0].cards.map(\.order) == [2048, 3072, 4096, 5120, 6144, 7168, 8192])

        let coerced = Dictionary(
            uniqueKeysWithValues: result.coercedFrontmatter.map { ($0.path, $0.fields) })
        for shape in shapes {
            #expect(
                coerced["\(lane)/\(shape.id)/index.md"] == [CoercedField(key: "order", raw: shape.raw)],
                "\(shape.frontmatter) should coerce with raw '\(shape.raw)'"
            )
        }
    }

    /// The rule holds one level up: an order-less lane sits right of every ranked one.
    @Test func orderlessLaneAppendsAtTheEndOfTheStrip() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let ranked = "90000000-0000-4000-8000-000000000001"
        let orderless = "10000000-0000-4000-8000-000000000002"

        try fixture.index("", "schema: 1\n")
        try fixture.index(ranked, "schema: 1\norder: 1024\n")
        try fixture.index(orderless, "schema: 1\n")

        let model = try BoardLoader.load(boardRoot: fixture.root).model
        #expect(model.lanes.map(\.id.rawValue) == [ranked, orderless])
        #expect(model.lanes.map(\.order) == [1024, 2048])
    }

    /// A missing `schema` below the root reads as 1 and records a coercion; the **root's** own
    /// missing `schema` is still the loud rejection (`missingSchemaThrows` above).
    @Test func missingSchemaBelowRootReadsAsOne() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()
        let card = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "order: 1024\ntitle: No Schema Lane\n")
        try fixture.index("\(lane)/\(card)", "order: 1024\ntitle: No Schema Card\n")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.model.lanes[0].schema == 1)
        #expect(result.model.lanes[0].cards[0].schema == 1)

        let paths = Set(result.coercedFrontmatter.map(\.path))
        #expect(paths == ["\(lane)/index.md", "\(lane)/\(card)/index.md"])
        #expect(result.coercedFrontmatter.allSatisfy { $0.fields == [CoercedField(key: "schema", raw: "")] })
    }

    /// A trash entry without a rank reads like every other order-less file. `order` decides nothing
    /// about where a trash row sits — `modified` does — so this is only about the rank it carries
    /// back out on a restore.
    @Test func orderlessTrashEntryReadsAsAppendAtEnd() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let ranked = "20000000-0000-4000-8000-000000000001"
        let orderless = "10000000-0000-4000-8000-000000000002"

        try fixture.index("", "schema: 1\n")
        try fixture.index(".trash/\(ranked)", "schema: 1\nkind: card\norder: 1024\n")
        try fixture.index(".trash/\(orderless)", "schema: 1\nkind: card\n")

        let model = try BoardLoader.load(boardRoot: fixture.root).model
        let byID = Dictionary(uniqueKeysWithValues: model.trash.map { ($0.id.rawValue, $0.order) })
        #expect(byID[ranked] == 1024)
        #expect(byID[orderless] == 2048)
    }

    /// **`hero` rides onto the card model, on both sides of the container boundary** (03-board-ui.md
    /// § Card face ▸ Hero image). The trash half is the one worth writing down: a trashed card is an
    /// ordinary card in a special place and draws the same face, so a snapshot that dropped the key
    /// on the way through `.trash/` would silently un-band every deleted card.
    ///
    /// The malformed case rides too, as a *shape* rather than a value: the coerce tier reports on it
    /// (`coercedFrontmatter`), and the face reads it as no hero at all.
    @Test func heroCarriesOntoCardsInBothContainers() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        let lane = uuidFolderName()
        let card = uuidFolderName()
        let pathy = uuidFolderName()
        let bare = uuidFolderName()
        let trashed = uuidFolderName()

        try fixture.index("", "schema: 1\n")
        try fixture.index(lane, "schema: 1\norder: 1024\n")
        try fixture.index("\(lane)/\(card)", "schema: 1\nkind: card\norder: 1024\nhero: sketch.png\n")
        try fixture.index("\(lane)/\(pathy)", "schema: 1\nkind: card\norder: 2048\nhero: art/sketch.png\n")
        try fixture.index("\(lane)/\(bare)", "schema: 1\nkind: card\norder: 3072\n")
        try fixture.index(".trash/\(trashed)", "schema: 1\nkind: card\nhero: cover.jpg\n")

        let result = try BoardLoader.load(boardRoot: fixture.root)
        let cards = Dictionary(uniqueKeysWithValues: result.model.lanes[0].cards.map { ($0.id.rawValue, $0.hero) })
        #expect(cards[card] == .valid("sketch.png"))
        #expect(cards[pathy] == .malformed(raw: "art/sketch.png"))
        #expect(cards[bare] == .missing)
        #expect(result.model.trash.map(\.hero) == [.valid("cover.jpg")])

        let byPath = Dictionary(uniqueKeysWithValues:
            result.coercedFrontmatter.map { ($0.path, $0.fields.map(\.key)) })
        #expect(byPath["\(lane)/\(pathy)/index.md"] == ["hero"])
        #expect(byPath["\(lane)/\(card)/index.md"] == nil)
    }
}

// MARK: - Encoding strictness

/// The loader decodes byte-faithfully (no NSString BOM-stripping) so the settled encoding
/// contract (01-storage-format.md § Fractal layout ▸ Rules) actually holds at load time: a
/// BOM'd file fails the frontmatter delimiter, a non-UTF-8 file is named as such — the same
/// strict decode `BoardWriter` uses, so a file can never load here and then refuse every write.
struct BoardLoaderEncodingTests {
    @Test func bomPrefixedBoardIndexIsRejected() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        var bytes = Data([0xEF, 0xBB, 0xBF])
        bytes.append(Data("---\nschema: 1\n---\nbody\n".utf8))
        try bytes.write(to: fixture.root.appendingPathComponent("index.md"))

        #expect { try BoardLoader.load(boardRoot: fixture.root) } throws: { error in
            guard let failure = error as? BoardLoadFailure,
                  case .unparseableYAML = failure.primary.reason else { return false }
            return failure.primary.path == "index.md"
        }
    }

    @Test func nonUTF8BoardIndexIsRejected() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }

        // "café" in ISO-8859-1 — 0xE9 is not valid UTF-8.
        var bytes = Data("---\nschema: 1\ntitle: caf".utf8)
        bytes.append(0xE9)
        bytes.append(Data("\n---\n".utf8))
        try bytes.write(to: fixture.root.appendingPathComponent("index.md"))

        #expect { try BoardLoader.load(boardRoot: fixture.root) } throws: { error in
            guard let failure = error as? BoardLoadFailure,
                  case let .unparseableYAML(message, _) = failure.primary.reason else { return false }
            return message == "file is not UTF-8"
        }
    }
}

// MARK: - The coerce tier's trace (01-storage-format.md § Frontmatter, ruled 2026-07-29)

/// **"A no-sensible-reading fallback logs"** — field, path, and raw text, carried as coerce-tier
/// entries in the integrity service's Defect stream:
///
/// > the one place where an observed-in-the-wild shape can later be promoted to a heuristic heal or a
/// > notice; no banner, no behavior change.
///
/// So these tests assert two things at once, and the second matters as much as the first: the fallback
/// is *reported*, and nothing about the board changed because of it — the field still renders its
/// default, the bytes on disk are still verbatim, and no heal is scheduled.
@Suite("BoardLoader ▸ coerce-tier fallbacks")
struct BoardLoaderCoercionTraceTests {

    /// The pure half first: which lenient fields report, and which deliberately do not.
    ///
    /// `schema` and `order` are the **refuse** tier — a malformed one fails the load loudly, so there is
    /// no silent recovery to leave a trace of — and `deleted`'s rule is presence-not-validity, so
    /// nothing falls back to a default there either.
    @Test("The document reports its lenient fallbacks, and only those")
    func theDocumentReportsItsLenientFallbacks() throws {
        let document = try FrontmatterDocument.parse("""
        ---
        schema: 1
        order: 1024
        title: [a, b]
        width: 1.5
        created: not-a-date
        icon: {a: b}
        deleted: also-not-a-date
        ---
        Body.
        """)

        let byKey = Dictionary(uniqueKeysWithValues: document.coercedFields.map { ($0.key, $0.raw) })
        #expect(Set(byKey.keys) == ["title", "width", "created", "icon"])
        #expect(byKey["width"] == "1.5", "the raw text as written — what a future heuristic would read")
        #expect(byKey["created"] == "not-a-date")
        #expect(byKey["deleted"] == nil, "presence, not validity, decides a tombstone")
    }

    @Test("A clean document reports nothing")
    func aCleanDocumentReportsNothing() throws {
        let document = try FrontmatterDocument.parse("""
        ---
        schema: 1
        order: 1024
        title: Fine
        width: 2
        ---
        """)
        #expect(document.coercedFields.isEmpty)
    }

    /// **A scalar of the wrong type is not a fallback** — it coerces to the text the author typed
    /// (`title: 2048` reads as "2048"), which is a *successful* reading and leaves no trace. Only "no
    /// sensible reading exists" does.
    @Test("A coerced scalar leaves no trace — it was read, not defaulted")
    func aCoercedScalarLeavesNoTrace() throws {
        let document = try FrontmatterDocument.parse("---\nschema: 1\ntitle: 2048\nwidth: \"3\"\n---\n")
        #expect(document.title.value == "2048")
        #expect(document.width.value == 3)
        #expect(document.coercedFields.isEmpty)
    }

    /// The loader's half: the path is attached at every level, because the rule is about fields and
    /// every level has them.
    @Test("The loader attaches the path, at every level")
    func theLoaderAttachesThePath() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let lane = uuidFolderName()
        let card = uuidFolderName()
        let trashed = uuidFolderName()
        try fixture.index("", "schema: 1\ntitle: [a, b]\n")
        try fixture.index(lane, "schema: 1\norder: 1024\nwidth: 1.5\n")
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\nicon: {x: y}\n")
        try fixture.index(".trash/\(trashed)", "schema: 1\norder: 1024\ncreated: nope\n")

        let reported = try BoardLoader.load(boardRoot: fixture.root).coercedFrontmatter
        let byPath = Dictionary(uniqueKeysWithValues: reported.map { ($0.path, $0.fields.map(\.key)) })

        #expect(byPath["index.md"] == ["title"])
        #expect(byPath["\(lane)/index.md"] == ["width"])
        #expect(byPath["\(lane)/\(card)/index.md"] == ["icon"])
        #expect(byPath[".trash/\(trashed)/index.md"] == ["created"])
    }

    /// **No behavior change** — the whole point of the tier. The fields render their defaults exactly as
    /// they did before anything was reported, and the bytes are preserved verbatim.
    @Test("Nothing about the board changes — defaults render, bytes stay")
    func nothingChanges() throws {
        let fixture = try BoardFixture()
        defer { fixture.tearDown() }
        let lane = uuidFolderName()
        try fixture.index("", "schema: 1\ntitle: Board\n")
        try fixture.index(lane, "schema: 1\norder: 1024\ntitle: [a, b]\nwidth: 0.5\n")
        let before = try Data(contentsOf: fixture.root.appendingPathComponent("\(lane)/index.md"))

        let result = try BoardLoader.load(boardRoot: fixture.root)
        let loaded = try #require(result.model.lanes.first)

        #expect(loaded.title.isMalformed, "the field still reads as malformed")
        #expect(loaded.title.value == nil, "and renders its default — the untitled placeholder")
        #expect(loaded.width.value == nil, "width falls back to 1 at the render layer, not here")
        #expect(try Data(contentsOf: fixture.root.appendingPathComponent("\(lane)/index.md")) == before,
                "read-side only: the loader never writes")
        #expect(result.warnings.isEmpty, "a coercion is not a stray warning")
    }

    /// **It is not healable work**, which is why it has no class: a class is a memo key and a
    /// banner-posture row in the engine, and inventing one would arm a memo against a repair nobody
    /// wrote. The other defects keep theirs.
    @Test("A coerce-tier defect has no heal class, and signs per field")
    func itHasNoHealClass() {
        let defect = IntegrityRules.Defect.coercedFrontmatter(CoercedFrontmatter(
            path: "lane/card/index.md",
            fields: [CoercedField(key: "width", raw: "1.5"), CoercedField(key: "icon", raw: "{}")]
        ))
        #expect(defect.healClass == nil)
        #expect(Set(defect.signatures) == [
            "coerce:lane/card/index.md:width",
            "coerce:lane/card/index.md:icon",
        ])
    }
}

// MARK: - The parse memo

private extension BoardFixture {

    /// A file's modification date, forced. Used wherever a test rewrites bytes without changing the
    /// byte *count*: the stamp rule is the subject there, and leaving it to the filesystem clock
    /// would make the assertion a race against timestamp resolution rather than a statement about
    /// mtime.
    func setModified(_ relativePath: String, to date: Date) throws {
        let folder = relativePath.isEmpty ? root : root.appendingPathComponent(relativePath, isDirectory: true)
        try FileManager.default.setAttributes(
            [.modificationDate: date],
            ofItemAtPath: folder.appendingPathComponent("index.md").path
        )
    }
}

/// A board with every container the walk has: a root, two lanes, two cards in the first, one
/// attachment, and one trashed card — so "the whole tree came out of the memo" is a claim about all
/// four levels rather than about lanes.
private func memoBoard() throws -> (fixture: BoardFixture, lane: String, card: String) {
    let fixture = try BoardFixture()
    let lane = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
    let card = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"

    try fixture.index("", "schema: 1\ntitle: Board\n")
    try fixture.index(lane, "schema: 1\norder: 1024\ntitle: Todo\n")
    try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ntitle: First\n")
    try fixture.index("\(lane)/cccccccc-cccc-4ccc-8ccc-cccccccccccc", "schema: 1\norder: 2048\ntitle: Second\n")
    try fixture.index(".trash/dddddddd-dddd-4ddd-8ddd-dddddddddddd", "schema: 1\norder: 1024\ntitle: Gone\n")
    try fixture.strayFile("\(lane)/\(card)/attachments/shot.png", contents: "png")
    return (fixture, lane, card)
}

/// Every `index.md` the board above holds — the number a warm walk must answer for without opening
/// one of them.
private let memoBoardIndexCount = 5

/// A whole-second modification date, forced onto a file wherever a test needs two stamps to compare
/// *exactly*. A date read back off the filesystem does not necessarily round-trip through
/// `setAttributes` bit for bit (`Date` is a `Double` and the syscall's is a `timespec`), and a test
/// about mtime equality must not turn into a test about that conversion.
private let pinnedMtime = Date(timeIntervalSince1970: 1_750_000_000)

/// **The walk memoizes its parse, never its result** (02-architecture.md § Live-reload resilience,
/// blessed 2026-07-31 — `BoardLoader.ParseMemo`).
///
/// Two claims that pull in opposite directions, which is why they are pinned side by side. The memo
/// has to actually save the reads: a walk over an untouched tree opens no `index.md` at all. And it
/// has to be undetectable in the answer, for the files it saved and — harder — for everything it
/// deliberately does not cover, which is every directory listing the walk makes. The equivalence half
/// is stated again over the golden fixture boards (`FixtureMemoEquivalenceTests`); this suite pins the
/// mechanism file by file, where a synthetic tree can be edited between two walks.
@Suite("BoardLoader ▸ the parse memo")
struct BoardLoaderParseMemoTests {

    @Test("A cold walk parses every index.md and reuses nothing")
    func aColdWalkParsesEverything() throws {
        let (fixture, _, _) = try memoBoard()
        defer { fixture.tearDown() }

        let counter = BoardLoader.ParseCounter()
        let result = try BoardLoader.load(boardRoot: fixture.root, counter: counter)

        #expect(counter.counts == .init(parsed: memoBoardIndexCount, reused: 0))
        #expect(result.memo.count == memoBoardIndexCount)
    }

    @Test("A walk over an untouched tree opens no index.md at all")
    func anUntouchedTreeIsAnsweredEntirelyFromTheMemo() throws {
        let (fixture, _, _) = try memoBoard()
        defer { fixture.tearDown() }

        let cold = try BoardLoader.load(boardRoot: fixture.root)

        let counter = BoardLoader.ParseCounter()
        let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)

        #expect(counter.counts == .init(parsed: 0, reused: memoBoardIndexCount))
        // The whole of the claim's other half: the cheap walk and the cold one are the same walk.
        #expect(warm.model == cold.model)
        #expect(warm.warnings == cold.warnings)
        #expect(warm.defects == cold.defects)
        #expect(warm.trashKinds == cold.trashKinds)
        #expect(warm.memo.count == cold.memo.count)
    }

    @Test("A single-file echo re-parses exactly that file")
    func oneEditedFileIsTheOnlyOneReParsed() throws {
        let (fixture, lane, card) = try memoBoard()
        defer { fixture.tearDown() }

        let cold = try BoardLoader.load(boardRoot: fixture.root)
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ntitle: Renamed\n")

        let counter = BoardLoader.ParseCounter()
        let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)

        #expect(counter.counts == .init(parsed: 1, reused: memoBoardIndexCount - 1))
        #expect(warm.model.lanes.first?.cards.first?.title.value == "Renamed")
        // And the cold answer is still the answer: the memo changed the cost, nothing else.
        #expect(warm.model == (try BoardLoader.load(boardRoot: fixture.root).model))
    }

    @Test("A same-size rewrite still re-parses, because the mtime moved")
    func aSameSizeRewriteReParses() throws {
        let (fixture, lane, card) = try memoBoard()
        defer { fixture.tearDown() }

        try fixture.setModified("\(lane)/\(card)", to: pinnedMtime)
        let cold = try BoardLoader.load(boardRoot: fixture.root)

        // "First" → "Third": identical byte count, so `size` alone would call this unchanged.
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ntitle: Third\n")
        try fixture.setModified("\(lane)/\(card)", to: pinnedMtime.addingTimeInterval(1))

        let counter = BoardLoader.ParseCounter()
        let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)

        #expect(counter.counts == .init(parsed: 1, reused: memoBoardIndexCount - 1))
        #expect(warm.model.lanes.first?.cards.first?.title.value == "Third")
    }

    /// **The heuristic's stated blind spot, pinned rather than discovered** (02-architecture.md: "The
    /// mtime+size trust is the git-index heuristic; a writer that defeats it — content changed, mtime
    /// and size both preserved — is outside the app's care").
    ///
    /// It is here so the boundary is a decision with a test on it: this is the one shape in which a
    /// memoized walk and a cold walk disagree, and the design says so out loud.
    @Test("A rewrite that preserves both mtime and size is trusted — the git-index heuristic's edge")
    func aRewritePreservingTheStampIsTrusted() throws {
        let (fixture, lane, card) = try memoBoard()
        defer { fixture.tearDown() }

        try fixture.setModified("\(lane)/\(card)", to: pinnedMtime)
        let cold = try BoardLoader.load(boardRoot: fixture.root)
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ntitle: Third\n")
        try fixture.setModified("\(lane)/\(card)", to: pinnedMtime)

        let counter = BoardLoader.ParseCounter()
        let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)

        #expect(counter.counts == .init(parsed: 0, reused: memoBoardIndexCount))
        #expect(warm.model.lanes.first?.cards.first?.title.value == "First")
        // And the very next walk that *does* see a moved stamp catches up — the window is one write,
        // not a standing state.
        try fixture.setModified("\(lane)/\(card)", to: pinnedMtime.addingTimeInterval(1))
        let next = try BoardLoader.load(boardRoot: fixture.root, memo: warm.memo)
        #expect(next.model.lanes.first?.cards.first?.title.value == "Third")
    }

    // MARK: Directory enumeration is never memoized

    @Test("An attachment arriving is seen by a walk that parsed nothing")
    func attachmentListingsStayFresh() throws {
        let (fixture, lane, card) = try memoBoard()
        defer { fixture.tearDown() }

        let cold = try BoardLoader.load(boardRoot: fixture.root)
        // An attachment never touches `index.md`, which is exactly why the memo must not cover the
        // listing that finds it.
        try fixture.strayFile("\(lane)/\(card)/attachments/second.png", contents: "png")

        let counter = BoardLoader.ParseCounter()
        let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)

        #expect(counter.counts == .init(parsed: 0, reused: memoBoardIndexCount))
        #expect(warm.model.lanes.first?.cards.first?.attachments == ["second.png", "shot.png"])
    }

    @Test("A loose file arriving beside an untouched index.md is still a defect")
    func looseFileDetectionStaysFresh() throws {
        let (fixture, lane, card) = try memoBoard()
        defer { fixture.tearDown() }

        let cold = try BoardLoader.load(boardRoot: fixture.root)
        #expect(cold.looseCardFiles.isEmpty)
        try fixture.strayFile("\(lane)/\(card)/notes.txt", contents: "loose")

        let counter = BoardLoader.ParseCounter()
        let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)

        #expect(counter.counts == .init(parsed: 0, reused: memoBoardIndexCount))
        #expect(warm.looseCardFiles.map(\.fileNames) == [["notes.txt"]])
    }

    @Test("A new card folder is discovered, and it is the only file the walk opens")
    func folderDiscoveryStaysFresh() throws {
        let (fixture, lane, _) = try memoBoard()
        defer { fixture.tearDown() }

        let cold = try BoardLoader.load(boardRoot: fixture.root)
        let arrival = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"
        try fixture.index("\(lane)/\(arrival)", "schema: 1\norder: 4096\ntitle: Arrived\n")

        let counter = BoardLoader.ParseCounter()
        let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo, counter: counter)

        #expect(counter.counts == .init(parsed: 1, reused: memoBoardIndexCount))
        #expect(warm.model.lanes.first?.cards.map(\.title.value) == ["First", "Second", "Arrived"])
    }

    @Test("A vanished card leaves the model, and its memo entry goes with it")
    func aVanishedItemLeavesTheMemo() throws {
        let (fixture, lane, card) = try memoBoard()
        defer { fixture.tearDown() }

        let cold = try BoardLoader.load(boardRoot: fixture.root)
        try FileManager.default.removeItem(at: fixture.root.appendingPathComponent("\(lane)/\(card)"))

        let warm = try BoardLoader.load(boardRoot: fixture.root, memo: cold.memo)

        #expect(warm.model.lanes.first?.cards.map(\.title.value) == ["Second"])
        #expect(warm.memo.count == memoBoardIndexCount - 1)
    }

    // MARK: Defects can never be answered from it

    @Test("A defective index.md is never memoized, so a still-broken file is re-read every walk")
    func aDefectIsNeverMemoized() throws {
        let (fixture, lane, card) = try memoBoard()
        defer { fixture.tearDown() }

        let healthy = try BoardLoader.load(boardRoot: fixture.root)
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\nlabels: [a, b\n")

        // The file does not change between these two walks, and neither of them can go quiet about
        // it: a defective `index.md` is never recorded, so there is nothing for a later walk to hit.
        // The healthy memo still spares the four files that *did* load, which is the point — the
        // defect costs one read, not a cold walk.
        for _ in 0..<2 {
            let counter = BoardLoader.ParseCounter()
            do throws(BoardLoadFailure) {
                _ = try BoardLoader.load(boardRoot: fixture.root, memo: healthy.memo, counter: counter)
                Issue.record("expected the broken card to fail the walk")
            } catch {
                #expect(error.defects.map(\.path) == ["\(lane)/\(card)/index.md"])
            }
            #expect(counter.counts == .init(parsed: 1, reused: memoBoardIndexCount - 1))
        }

        // And the aggregate is the cold aggregate, defect for defect.
        do throws(BoardLoadFailure) {
            _ = try BoardLoader.load(boardRoot: fixture.root, memo: healthy.memo)
            Issue.record("expected the broken card to fail the walk")
        } catch let warm {
            do throws(BoardLoadFailure) {
                _ = try BoardLoader.load(boardRoot: fixture.root)
                Issue.record("expected the broken card to fail the walk")
            } catch let cold {
                #expect(warm.defects == cold.defects)
            }
        }
    }

    @Test("A repaired file re-parses and rejoins the board")
    func aRepairedFileReParses() throws {
        let (fixture, lane, card) = try memoBoard()
        defer { fixture.tearDown() }

        let healthy = try BoardLoader.load(boardRoot: fixture.root)
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\nlabels: [a, b\n")
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\ntitle: Repaired\n")

        let warm = try BoardLoader.load(boardRoot: fixture.root, memo: healthy.memo)
        #expect(warm.model.lanes.first?.cards.first?.title.value == "Repaired")
    }

    @Test("A skip is recomputed from a fresh parse, memo or no memo")
    func aSkipComposesWithTheMemo() throws {
        let (fixture, lane, card) = try memoBoard()
        defer { fixture.tearDown() }

        let healthy = try BoardLoader.load(boardRoot: fixture.root)
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\nlabels: [a, b\n")
        let skips: Set<String> = ["\(lane)/\(card)/index.md"]

        let counter = BoardLoader.ParseCounter()
        let warm = try BoardLoader.load(
            boardRoot: fixture.root, skipping: skips, memo: healthy.memo, counter: counter)
        let cold = try BoardLoader.load(boardRoot: fixture.root, skipping: skips)

        // A skipped path is a defect path, so it was never in the memo and is read on every walk.
        #expect(counter.counts.parsed == 1)
        #expect(warm.model == cold.model)
        #expect(warm.warnings == cold.warnings)
        #expect(warm.warnings.contains(.userSkipped(path: "\(lane)/\(card)/index.md")))
        // The skipped item is out of the board and out of the memo, both walks alike.
        #expect(warm.memo.count == memoBoardIndexCount - 1)
    }

    @Test("A skipped path stays skipped across a memoized reload")
    func aSkipHoldsAcrossReloads() throws {
        let (fixture, lane, card) = try memoBoard()
        defer { fixture.tearDown() }

        let healthy = try BoardLoader.load(boardRoot: fixture.root)
        try fixture.index("\(lane)/\(card)", "schema: 1\norder: 1024\nlabels: [a, b\n")
        let skips: Set<String> = ["\(lane)/\(card)/index.md"]

        let first = try BoardLoader.load(boardRoot: fixture.root, skipping: skips, memo: healthy.memo)
        let second = try BoardLoader.load(boardRoot: fixture.root, skipping: skips, memo: first.memo)

        #expect(second.model == first.model)
        #expect(second.warnings == first.warnings)
        #expect(second.model.lanes.first?.cards.map(\.title.value) == ["Second"])
    }
}
