Unify the trashed universe on the trash rows
02 rules the trashed side has exactly one definition: the set with trash rows — a card carrying its own deleted: under a tombstoned lane is in neither universe, so an anchor or selection can never survive on an item that renders nowhere. The membership rule now lives once, in Liveness.walk, and ItemReferenceSet.idUniverse, TrashModel.entries, paths, and emptyTrashTargets all derive from it — the old lane-OR-card logic that admitted subsumed cards to the trashed side is gone, and the two universes deliberately no longer partition the board. Put Back, Delete Immediately, and Empty Trash outcomes are unchanged: a tombstoned lane still moves and purges whole, its nested tombstones with it. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -26,6 +26,54 @@ public enum Liveness: String, Codable, Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The one definition of a side
|
||||
|
||||
extension Liveness {
|
||||
|
||||
/// Every item `snapshot` has on this side, visited in **board order** — lanes left to right,
|
||||
/// each lane immediately before its own cards — as the lane it lives under and, for a card, the
|
||||
/// card itself.
|
||||
///
|
||||
/// **This is the definition, and it is the only one** (02-architecture.md § Changes from Kanban,
|
||||
/// settled: "universe and rows are one function, never a broader set with a pointer-side
|
||||
/// subset"). Everything that asks what is on a side is this walk with a different accumulator:
|
||||
/// `ItemReferenceSet.idUniverse` collects ids, `TrashModel.entries` builds the trash's rows,
|
||||
/// `TrashModel.paths` and `emptyTrashTargets` build folders. Two spellings of the rule would be
|
||||
/// two things to keep in step, and the one they would eventually disagree about is precisely the
|
||||
/// item below.
|
||||
///
|
||||
/// **The whole rule is the `continue`: a tombstoned lane subsumes its subtree.** It contributes
|
||||
/// one item to the trashed side — its own row — and its cards contribute nothing to *either*
|
||||
/// side, whatever their own flags say. That is 03-board-ui.md § Trash's absolute ancestor walk
|
||||
/// stated as code: "a card that carries its own `deleted:` under a tombstoned lane has **no row
|
||||
/// of its own**".
|
||||
///
|
||||
/// **So the two sides do not partition the board, and that is the point.** A card beneath a
|
||||
/// tombstoned lane is in *neither* universe, because it renders nowhere — no row, no membership.
|
||||
/// A selection, a drag, a pending cut, a range anchor or a navigation head can therefore never
|
||||
/// survive a reload sitting on something no surface would draw, and 04-interactions.md § Search's
|
||||
/// hidden-cards-leave-the-selection rule and 02's reload-survival rule stay one rule rather than
|
||||
/// two that happen to agree.
|
||||
///
|
||||
/// Non-escaping and accumulator-driven rather than array-returning: the callers below run on
|
||||
/// every reload and on every menu validation, and none of them wants a board-sized copy of the
|
||||
/// model to throw away.
|
||||
func walk(_ snapshot: BoardModel, visiting visit: (Lane, Card?) -> Void) {
|
||||
for lane in snapshot.lanes {
|
||||
if lane.isDeleted {
|
||||
// The subsumption, both halves of it: the lane is a trash row, and its cards are
|
||||
// nobody's — so the loop below never runs for them.
|
||||
if self == .trashed { visit(lane, nil) }
|
||||
continue
|
||||
}
|
||||
if self == .live { visit(lane, nil) }
|
||||
for card in lane.cards where Liveness(isDeleted: card.isDeleted) == self {
|
||||
visit(lane, card)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ItemReferenceSet
|
||||
|
||||
/// A set of UUIDs over the snapshot plus the liveness side it lives on — **the one shape every
|
||||
@@ -98,32 +146,27 @@ public struct ItemReferenceSet: Sendable, Equatable {
|
||||
/// item that is tombstoned or vanishes externally before paste drops out of the pending
|
||||
/// cut"), and so does drag membership (04 ▸ Drag and drop's emptied-drag rule).
|
||||
///
|
||||
/// The liveness that is matched is **effective — ancestor-walked** (settled): a card counts as
|
||||
/// trashed if its own flag *or its lane's* says so. Tombstoning a lane therefore ejects its
|
||||
/// cards from a live set even though their own flags never changed — the card renders nowhere
|
||||
/// once 03-board-ui.md collapses the lane to a single trash entry, and nothing invisible may
|
||||
/// stay selected, drag-included, or pending-cut.
|
||||
/// The liveness that is matched is **effective — ancestor-walked** (settled), and the trashed
|
||||
/// side is exactly the trash's rows: `Liveness.walk` is the one definition both sides read.
|
||||
/// Tombstoning a lane therefore ejects its cards from a live set even though their own flags
|
||||
/// never changed — and does **not** hand them to a trashed set, because the lane's single entry
|
||||
/// subsumes them (03-board-ui.md). A card under a tombstoned lane renders nowhere on either
|
||||
/// side, and nothing invisible may stay selected, drag-included, or pending-cut.
|
||||
public func resolved(against snapshot: BoardModel) -> ItemReferenceSet {
|
||||
guard !ids.isEmpty else { return self }
|
||||
return constrained(to: Self.idUniverse(of: snapshot, on: liveness))
|
||||
}
|
||||
|
||||
/// Every id in `snapshot` whose **effective** liveness is `side` — the reload direction's
|
||||
/// universe, and the only place the ancestor walk lives.
|
||||
/// Every id in `snapshot` on `side` — the reload direction's universe.
|
||||
///
|
||||
/// A lane contributes itself on the side its own flag names, and each of its cards on the side
|
||||
/// `lane.isDeleted || card.isDeleted` names — the walk being one level deep is the whole of it,
|
||||
/// because the tree is (01-storage-format.md § Fractal layout: board → lane → card).
|
||||
/// One line over `Liveness.walk`, which is where the rule itself lives and is stated: the live
|
||||
/// side is the live lanes and their unflagged cards, and the trashed side is *exactly* the trash's
|
||||
/// rows — tombstoned lanes, plus cards carrying their own `deleted:` under a live lane. Nothing
|
||||
/// else is in either, so a card hidden beneath a tombstoned lane belongs to no universe and no
|
||||
/// set may go on referencing it.
|
||||
static func idUniverse(of snapshot: BoardModel, on side: Liveness) -> Set<ItemID> {
|
||||
var universe: Set<ItemID> = []
|
||||
for lane in snapshot.lanes {
|
||||
if Liveness(isDeleted: lane.isDeleted) == side {
|
||||
universe.insert(lane.id)
|
||||
}
|
||||
for card in lane.cards where Liveness(isDeleted: lane.isDeleted || card.isDeleted) == side {
|
||||
universe.insert(card.id)
|
||||
}
|
||||
}
|
||||
side.walk(snapshot) { lane, card in universe.insert(card?.id ?? lane.id) }
|
||||
return universe
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user