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:
@@ -100,9 +100,11 @@ public struct Selection: Sendable, Equatable {
|
|||||||
/// homogeneous-by-liveness invariant true across reloads so menu validation never sees a
|
/// homogeneous-by-liveness invariant true across reloads so menu validation never sees a
|
||||||
/// mixed selection.
|
/// mixed selection.
|
||||||
///
|
///
|
||||||
/// The match is on the item's *own* tombstone flag. A live card sitting under a tombstoned lane
|
/// The liveness that is matched is **effective — ancestor-walked** (settled): a card counts as
|
||||||
/// therefore survives re-resolution even though the board does not render it — the loader keeps
|
/// trashed if its own flag *or its lane's* says so. Tombstoning a lane therefore ejects its
|
||||||
/// it in the snapshot, and "still present, same side" is the rule as written.
|
/// 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 {
|
public func resolved(against snapshot: BoardModel) -> Selection {
|
||||||
guard !ids.isEmpty else { return self }
|
guard !ids.isEmpty else { return self }
|
||||||
|
|
||||||
@@ -113,7 +115,7 @@ public struct Selection: Sendable, Equatable {
|
|||||||
survivors.insert(lane.id)
|
survivors.insert(lane.id)
|
||||||
}
|
}
|
||||||
for card in lane.cards where ids.contains(card.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)
|
survivors.insert(card.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -393,6 +393,25 @@ struct BoardStoreTests {
|
|||||||
#expect(store.selection.ids == [ItemID(rawValue: Ident.card2)])
|
#expect(store.selection.ids == [ItemID(rawValue: Ident.card2)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("Tombstoning a lane ejects its cards from a live selection — liveness is effective")
|
||||||
|
func selectionEjectsCardsUnderATombstonedLane() async throws {
|
||||||
|
let fixture = try makeBoard()
|
||||||
|
defer { fixture.tearDown() }
|
||||||
|
let store = try BoardStore(rootURL: fixture.root)
|
||||||
|
store.select([ItemID(rawValue: Ident.card1), ItemID(rawValue: Ident.card2)], liveness: .live)
|
||||||
|
|
||||||
|
try fixture.item(Ident.lane1, tombstoned(order: "1024", title: "Lane one"))
|
||||||
|
store.handleWatcherEvent(.treeChanged(.foreign))
|
||||||
|
await store.awaitQuiescence()
|
||||||
|
|
||||||
|
// The cards' own flags never changed, but their lane's did — and liveness is
|
||||||
|
// ancestor-walked (02, settled): the cards render nowhere once 03 collapses the lane to
|
||||||
|
// a single trash entry, and nothing invisible may stay selected.
|
||||||
|
let survivor = lane(Ident.lane1, in: store.snapshot)?.cards.first { $0.id.rawValue == Ident.card1 }
|
||||||
|
#expect(survivor?.isDeleted == false, "the card's own flag is untouched")
|
||||||
|
#expect(store.selection.ids.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
@Test("A selection can resolve to nothing, and nothing is invented to replace it")
|
@Test("A selection can resolve to nothing, and nothing is invented to replace it")
|
||||||
func selectionCanResolveToNothing() async throws {
|
func selectionCanResolveToNothing() async throws {
|
||||||
let fixture = try makeBoard()
|
let fixture = try makeBoard()
|
||||||
|
|||||||
Reference in New Issue
Block a user