Lanes delete into the trash — storage, loader, writer, and undo

Phase 1 of the lanes-in-trash card (2026-07-29 ruling, docs led the
code): lane delete is a move into .trash/ with the subtree intact,
arriving at top trash rank — no destructive delete remains outside
the trash.

TrashedLane opaque unit (id/schema/title/order/heldCards) beside
trash cards — deliberately not a Lane, so no card-shaped surface can
believe an empty subtree. Loader's trash walk trusts the kind VALUE
(lane → opaque unit w/ held-card count counted at the loader's own
unit; card → ordinary card; absent/unrecognized → UUID-children
shape, empty-kindless falls to card per 01's honest limit). Writer:
moveIntoTrash generalized with kind passed never derived (an empty
lane would re-derive as card), deleteLaneToTrash mints against the
whole-container rank ladder. Retired: migrateTombstonedLane (lane
deleted: now ignored — loads live, bytes inert, tolerate-tier
warning), removeLane, captureSubtree/recreateSubtree and the
subtree-snapshot machinery. Undo inverse = move back to captured
strip position, redo replays at captured trash rank. Purge walks
lane subtrees; TrashModel.Freight phrases confirms with lane freight
("…and its 5 cards"). ItemPath gains .trashLane; resolve interleaves
the trash by rank; SearchFilter matches lane rows by title only.
Trashed-lane card windows dismiss and pending cuts void via the
ordinary vanish rule — no new plumbing.

Phase 2 (rendering, selection grammar, drag, a11y, agent guide)
follows. Both schemes 1858 tests / 318 suites green.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-30 16:16:57 -04:00
parent 785ef5fe14
commit 8014bde7c6
21 changed files with 1357 additions and 910 deletions
+24
View File
@@ -82,6 +82,30 @@ struct CardWindowFateTests {
#expect(CardWindowHost.cardWindowFate(cardID: Ident.card1, in: snapshot) == .dismisses)
}
/// The lane's own delete is a *move* now (03 § Trash, re-ruled 2026-07-29), and the dismissal
/// still falls out of the same absence rather than needing a rule: a trashed lane is an opaque
/// unit, so its cards are not in the snapshot at all not under a lane, not among the trash's
/// cards and the walk simply does not find them.
@Test("Trashing a card's lane dismisses its window too — the freight leaves the snapshot with it")
func aLaneTrashedDismissesItsCards() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try BoardWriter.deleteLaneToTrash(
at: fixture.url(Ident.lane1), inBoard: fixture.root, order: -1024
)
let snapshot = try BoardLoader.load(boardRoot: fixture.root).model
#expect(snapshot.trashedLanes.map(\.id.rawValue) == [Ident.lane1])
#expect(!snapshot.trash.contains { $0.id.rawValue == Ident.card1 })
#expect(CardWindowHost.cardWindowFate(cardID: Ident.card1, in: snapshot) == .dismisses)
// And the same absence is what ejects it from every item-referencing set selection, the
// pending cut, drag membership (02-architecture.md § Live-reload resilience).
#expect(!ItemContainer.board.ids(in: snapshot).contains(ItemID(rawValue: Ident.card1)))
#expect(!ItemContainer.trash.ids(in: snapshot).contains(ItemID(rawValue: Ident.card1)))
#expect(ItemContainer.trash.ids(in: snapshot).contains(ItemID(rawValue: Ident.lane1)),
"the lane row itself is in the trash's universe")
}
@Test("A card that is not in this board's snapshot dismisses — the cross-board move")
func anAbsentCardDismisses() throws {
let fixture = try makeBoard()