Realign code with the 2026-07-31 rulings
The trash sorts by modified descending — the arrival rank mint retires (Ranks.isOrderedForTrash one comparator, loader + merged order agree; the legacy deleted: migration stamps modified from the tombstone timestamp where parseable; delete undo steps validate existence-only; agent guide v8). Trash selection goes kind-blind — ranges, marquee, Select All, and the successor walk sweep both kinds; the guard moves to the exits (mixed-payload drop refusal, copy/cut validation). The copy stamping preflight widens back to comment depth (load-scoped posture — the board always loads, the gesture refuses whole). Fixes a latent no-op: trashed-lane drag restore never fired (DragSession.beginLanes hard-coded the board container). 2403 tests in 413 suites green. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
@@ -25,10 +25,13 @@ private enum More {
|
||||
|
||||
/// A card or lane whose title and body are **independently controlled** — which `Item.rich` cannot
|
||||
/// be, since its body quotes its title, and a title-or-body test needs the two to disagree.
|
||||
private func item(order: String, title: String?, body: String) -> String {
|
||||
private func item(order: String, title: String?, body: String, modified: String? = nil) -> String {
|
||||
var lines = ["---", "schema: 1"]
|
||||
if let title { lines.append("title: \(title)") }
|
||||
lines.append("order: \(order)")
|
||||
// The trash sorts by `modified` descending (01-storage-format.md § Deletion, re-ruled
|
||||
// 2026-07-31), so a trash fixture states its column order here rather than in `order`.
|
||||
if let modified { lines.append("modified: \(modified)") }
|
||||
lines.append("---")
|
||||
return lines.joined(separator: "\n") + "\n" + body + "\n"
|
||||
}
|
||||
@@ -109,12 +112,17 @@ private func makeTrashBoard() throws -> WriterFixture {
|
||||
let fixture = try WriterFixture()
|
||||
try fixture.item("", Item.board)
|
||||
try fixture.item(Ident.lane1, item(order: "1024", title: "Todo", body: "Inbox lane."))
|
||||
try fixture.item(".trash/\(Ident.card1)", item(order: "1024", title: "Fix login", body: "Auth."))
|
||||
// The column reads [card1, laneX, card2] — by their stamps, both kinds interleaved.
|
||||
try fixture.item(
|
||||
".trash/\(Ident.card1)",
|
||||
item(order: "1024", title: "Fix login", body: "Auth.", modified: "2026-05-05T09:00:00Z"))
|
||||
try fixture.item(
|
||||
".trash/\(More.laneX)",
|
||||
"---\nschema: 1\ntitle: Archive\norder: 1536\nkind: lane\n---\nOld login notes.\n"
|
||||
"---\nschema: 1\ntitle: Archive\norder: 1536\nmodified: 2026-05-03T09:00:00Z\nkind: lane\n---\nOld login notes.\n"
|
||||
)
|
||||
try fixture.item(".trash/\(Ident.card2)", item(order: "2048", title: "Polish", body: "Wording."))
|
||||
try fixture.item(
|
||||
".trash/\(Ident.card2)",
|
||||
item(order: "2048", title: "Polish", body: "Wording.", modified: "2026-05-01T09:00:00Z"))
|
||||
return fixture
|
||||
}
|
||||
|
||||
@@ -335,19 +343,20 @@ struct SearchFilterOrderTests {
|
||||
/// "The row matches the search filter by lane title only" (03-board-ui.md § Trash) — the opaque
|
||||
/// unit's own rule, and the one place a lane *is* filtered: `laneX`'s body carries the query and
|
||||
/// the row still leaves the column, because its body is not on screen.
|
||||
@Test("A trashed lane row filters by title alone, and its own list is kind-scoped")
|
||||
@Test("A trashed lane row filters by title alone")
|
||||
func trashLaneRowsFilterByTitle() throws {
|
||||
let fixture = try makeTrashBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let model = try load(fixture)
|
||||
|
||||
#expect(SelectionGrammar.order(of: .lane, in: .trash, snapshot: model) == [laneX])
|
||||
#expect(SelectionGrammar.trashLanes(in: model) == [laneX])
|
||||
#expect(SelectionGrammar.trashLanes(in: model, filter: SearchFilter(query: "archive")) == [laneX])
|
||||
// The body says "login". The row does not.
|
||||
#expect(SelectionGrammar.trashLanes(in: model, filter: SearchFilter(query: "login")).isEmpty)
|
||||
// And the kind-scoped lists are disjoint slices of one rank order, which is what makes a
|
||||
// ⇧-range skip the other kind (04-interactions.md ▸ The trash).
|
||||
// And the ranging grammar's list is the whole column, both kinds — kind-blind trash selection
|
||||
// (04-interactions.md ▸ The trash, re-ruled 2026-07-31).
|
||||
#expect(SelectionGrammar.trashRows(in: model) == [card1, laneX, card2])
|
||||
#expect(SelectionGrammar.order(of: .lane, in: .trash, snapshot: model) == [card1, laneX, card2])
|
||||
}
|
||||
|
||||
@Test("The trash's visible universe is its matching rows, both kinds")
|
||||
@@ -371,7 +380,7 @@ struct SearchFilterOrderTests {
|
||||
defer { fixture.tearDown() }
|
||||
let model = try load(fixture)
|
||||
|
||||
// Both kinds, interleaved purely by rank (03-board-ui.md § Trash).
|
||||
// Both kinds, interleaved by `modified` descending (03-board-ui.md § Trash).
|
||||
#expect(TrashLaneView.rendered(model, filter: .inactive).map(\.id) == [card1, laneX, card2])
|
||||
#expect(TrashLaneView.rendered(model, filter: SearchFilter(query: "login")).map(\.id) == [card1])
|
||||
// The column and the arrow grammar cannot disagree about what is on screen.
|
||||
@@ -431,7 +440,8 @@ struct SearchFilterStoreTests {
|
||||
store.transient.isTrashVisible = true
|
||||
store.select([card2], in: .trash)
|
||||
store.selectAll()
|
||||
#expect(store.selection.ids == [card1, card2])
|
||||
// Every visible *row*, both kinds — kind-blind since 2026-07-31.
|
||||
#expect(store.selection.ids == [card1, laneX, card2])
|
||||
|
||||
store.select([card1], in: .trash)
|
||||
store.searchQuery = "login"
|
||||
|
||||
Reference in New Issue
Block a user