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:
2026-07-31 18:35:07 -04:00
parent 542ab169a3
commit bec75e4282
37 changed files with 1200 additions and 551 deletions
+20 -10
View File
@@ -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"