Selection liveness is effective — ancestor-walked (ratified)

The parallel design session settled the tombstoned-ancestor question:
a card counts as trashed if its own flag or its lane's says so, so
tombstoning a lane ejects its cards from a live selection — they render
nowhere once the lane collapses to a single trash entry, and nothing
invisible may stay selected, drag-included, or pending-cut. One-line
predicate change in Selection.resolved(against:), plus the test.

Full BoardStore suite green (15 tests).

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
2026-07-26 19:48:12 -04:00
parent f0e1738964
commit cb86316506
2 changed files with 25 additions and 4 deletions
+6 -4
View File
@@ -100,9 +100,11 @@ public struct Selection: Sendable, Equatable {
/// homogeneous-by-liveness invariant true across reloads so menu validation never sees a
/// mixed selection.
///
/// The match is on the item's *own* tombstone flag. A live card sitting under a tombstoned lane
/// therefore survives re-resolution even though the board does not render it the loader keeps
/// it in the snapshot, and "still present, same side" is the rule as written.
/// 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 selection 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.
public func resolved(against snapshot: BoardModel) -> Selection {
guard !ids.isEmpty else { return self }
@@ -113,7 +115,7 @@ public struct Selection: Sendable, Equatable {
survivors.insert(lane.id)
}
for card in lane.cards where ids.contains(card.id) {
if Liveness(isDeleted: card.isDeleted) == liveness {
if Liveness(isDeleted: lane.isDeleted || card.isDeleted) == liveness {
survivors.insert(card.id)
}
}