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:
@@ -502,7 +502,8 @@ public enum IntegrityRules: Sendable {
|
||||
public enum Defect: Sendable, Equatable {
|
||||
/// A card holding files that belong in its `attachments/` (the loose-file carve-out).
|
||||
case looseCardFiles(LooseCardFiles)
|
||||
/// An item carrying a legacy `deleted:` key (the retired tombstone model's migration input).
|
||||
/// A **card** carrying a legacy `deleted:` key (the retired tombstone model's surviving
|
||||
/// migration input; a lane's key is inert and tolerated instead).
|
||||
case legacyTombstone(LegacyTombstone)
|
||||
/// A claimed board-root name held by the wrong kind of node (ruled 2026-07-29).
|
||||
case claimedNameSquatted(ClaimedNameSquatter)
|
||||
@@ -567,7 +568,7 @@ public enum IntegrityRules: Sendable {
|
||||
case let .looseCardFiles(work):
|
||||
work.fileNames.map { "loose:\(work.laneID.rawValue)/\(work.cardID.rawValue)/\($0)" }
|
||||
case let .legacyTombstone(work):
|
||||
["tombstone:\(work.laneID.rawValue)/\(work.cardID?.rawValue ?? "")"]
|
||||
["tombstone:\(work.laneID.rawValue)/\(work.cardID.rawValue)"]
|
||||
case let .claimedNameSquatted(work):
|
||||
// The node *kind* is part of the picture: a squatter replaced by a different kind
|
||||
// of squatter is a new defect, and a heal that failed on one has no claim to have
|
||||
@@ -846,35 +847,30 @@ public struct LooseCardFiles: Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// One item found carrying a legacy `deleted:` key — everything its migration and notice need, and
|
||||
/// nothing more (01-storage-format.md § Deletion: "Legacy `deleted:` keys migrate on load-and-write,
|
||||
/// never destroy").
|
||||
/// One **card** found carrying a legacy `deleted:` key — everything its migration and notice need,
|
||||
/// and nothing more (01-storage-format.md § Deletion: "a card carrying `deleted:` is relocated into
|
||||
/// `.trash/` (key removed)").
|
||||
///
|
||||
/// **Cards only, since 2026-07-29.** The rule used to have two halves, and the lane half is retired
|
||||
/// wholesale: "a lane carrying `deleted:` simply loads live with the key ignored — no migration
|
||||
/// machinery, no key-strip write, no notice". A lane's key is now the tolerate tier's business
|
||||
/// (`LoadWarning.laneLevelDeletedIgnored`), which is why there is no `kind` here to switch on: the
|
||||
/// one surviving act is a move, and a payload that could spell the other one would be a defect
|
||||
/// nothing can heal.
|
||||
///
|
||||
/// The path is carried as its identity components rather than as a URL — `LooseCardFiles`'
|
||||
/// convention, for its reason. `title` is the item's as written, `nil` for an untitled one.
|
||||
/// convention, for its reason. `title` is the card's as written, `nil` for an untitled one.
|
||||
public struct LegacyTombstone: Sendable, Equatable {
|
||||
/// Which migration this item takes — the two are genuinely different acts, not one act at two
|
||||
/// levels: a card *moves* (into `.trash/`, at a minted top-of-trash rank) and a lane stays
|
||||
/// exactly where it is (the key is stripped and it returns live).
|
||||
public enum Kind: Sendable, Equatable {
|
||||
case card
|
||||
case lane
|
||||
}
|
||||
|
||||
public let kind: Kind
|
||||
|
||||
/// The lane's own identity for `.lane`; the card's **containing** lane for `.card` — the context
|
||||
/// the relocation needs to find the folder at all.
|
||||
/// The card's **containing** lane — the context the relocation needs to find the folder at all.
|
||||
public let laneID: ItemID
|
||||
|
||||
/// The card's identity for `.card`, `nil` for `.lane`. Two fields rather than an enum payload so
|
||||
/// the common "which folder is this" question is one path join at every call site.
|
||||
public let cardID: ItemID?
|
||||
/// The card itself.
|
||||
public let cardID: ItemID
|
||||
|
||||
public let title: String?
|
||||
|
||||
public init(kind: Kind, laneID: ItemID, cardID: ItemID?, title: String?) {
|
||||
self.kind = kind
|
||||
public init(laneID: ItemID, cardID: ItemID, title: String?) {
|
||||
self.laneID = laneID
|
||||
self.cardID = cardID
|
||||
self.title = title
|
||||
|
||||
Reference in New Issue
Block a user