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
+83 -42
View File
@@ -23,12 +23,16 @@ private enum More {
/// A lane already sitting in `<root>/.trash/` the opaque unit, `kind: lane` being the only thing
/// that tells it from a card in the flat container (01-storage-format.md § Deletion).
private func trashedLane(order: String, title: String) -> String {
///
/// `modified` is the row's **position** since 2026-07-31 (the trash sorts by it, descending), so it
/// is a fixture parameter rather than the afterthought it was while ranks did the ordering.
private func trashedLane(order: String, title: String, modified: String = "2026-05-01T09:00:00Z") -> String {
"""
---
schema: 1
title: \(title)
order: \(order)
modified: \(modified)
kind: lane
project: lanework # agent overlay
---
@@ -39,7 +43,7 @@ private func trashedLane(order: String, title: String) -> String {
/// A card already sitting in `<root>/.trash/` an ordinary card in a special place, with an unknown
/// key so the verbatim-preservation claims have something to preserve.
private func trashResident(order: String, title: String) -> String {
private func trashResident(order: String, title: String, modified: String = "2026-05-01T09:00:00Z") -> String {
"""
---
schema: 1
@@ -47,6 +51,7 @@ private func trashResident(order: String, title: String) -> String {
order: \(order)
project: lanework # agent overlay
created: 2026-01-01T09:00:00Z
modified: \(modified)
---
\(title) body.
@@ -83,8 +88,14 @@ private func makeBoard() throws -> WriterFixture {
try fixture.item("\(Ident.lane2)/\(Ident.card3)", Item.rich(order: "1024", title: "Third"))
try fixture.item(Ident.lane3, Item.rich(order: "3072", title: "Done"))
try fixture.item("\(Ident.lane3)/\(Ident.card4)", Item.rich(order: "1024", title: "Fourth"))
try fixture.item(".trash/\(Ident.indexless)", trashResident(order: "1024", title: "Trashed"))
try fixture.item(".trash/\(More.newer)", trashResident(order: "512", title: "Newer"))
// Their *stamps* are what orders them now "Newer" is the newer one, and its `order` is the
// lane rank it carried in, deliberately disagreeing with the column position.
try fixture.item(
".trash/\(Ident.indexless)",
trashResident(order: "1024", title: "Trashed", modified: "2026-05-01T09:00:00Z"))
try fixture.item(
".trash/\(More.newer)",
trashResident(order: "512", title: "Newer", modified: "2026-05-02T09:00:00Z"))
return fixture
}
@@ -172,34 +183,46 @@ struct DeleteCardTests {
#expect(try document(fixture, ".trash/\(Ident.card1)").modified.value != nil)
}
@Test("Entry is at the top: the rank is minted above the current topmost")
/// **Entry is at the top, and the stamp is what puts it there** (03 Trash, re-ruled
/// 2026-07-31): no rank is minted, so the card's `order` arrives exactly as it left its lane and
/// the fresh `modified` does the positioning.
@Test("Entry is at the top: the fresh stamp outranks every resident, and `order` is untouched")
func entryIsAtTheTop() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
// The trash's current top is `newer` at 512.
// The trash's current top is `newer`, by its stamp its `order` (512) is the smaller of the
// two, which under the retired rule would have been the reason and now is a coincidence.
#expect(try loaded(fixture).trash.map(\.id) == [newer, trashed])
store.delete([card1])
let landed = try #require(try document(fixture, ".trash/\(Ident.card1)").order.value)
#expect(landed < 512, "03 ▸ Trash: every arrival mints an `order` rank above the current top")
let arrived = try document(fixture, ".trash/\(Ident.card1)")
#expect(arrived.order == .valid(1024), "the lane rank rides along; nothing is minted")
let stamp = try #require(arrived.modified.value)
let residentStamp = try #require(try document(fixture, ".trash/\(More.newer)").modified.value)
#expect(stamp > residentStamp)
#expect(try loaded(fixture).trash.map(\.id) == [card1, newer, trashed],
"newest-first falls out of ordinary ranks — no timestamp sort")
"newest-first falls out of the stamp — no rank anywhere in the container")
}
@Test("A multi-card delete is one bracket, each arrival above the one before it")
func batchLandsNewestOnTop() throws {
/// **A batch shares one instant, so the deterministic tail orders it** (01 § Deletion: "Ties
/// break by title (case-insensitive), then folder name the deterministic tail"). `modified`
/// serializes at whole-second granularity, so two cards deleted in one bracket carry the same
/// stamp by construction; the run still sorts above every resident, and *within* the run the
/// titles decide "First" before "Second". That is the tail doing exactly its job, and it is the
/// honest reading of two deletions that really did happen at the same time.
@Test("A multi-card delete is one bracket; the run lands on top and the tail orders it")
func batchLandsOnTopOrderedByTheTail() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
store.delete([card1, card2])
let first = try #require(try document(fixture, ".trash/\(Ident.card1)").order.value)
let second = try #require(try document(fixture, ".trash/\(Ident.card2)").order.value)
#expect(second < first)
#expect(try loaded(fixture).trash.map(\.id) == [card2, card1, newer, trashed])
#expect(try document(fixture, ".trash/\(Ident.card1)").order == .valid(1024))
#expect(try document(fixture, ".trash/\(Ident.card2)").order == .valid(2048))
#expect(try loaded(fixture).trash.map(\.id) == [card1, card2, newer, trashed])
}
@Test("The selection moves to the successor sibling, immediately")
@@ -312,27 +335,30 @@ struct DeleteLaneTests {
#expect(store.banners.oneShots.isEmpty)
}
/// "Every arrival lands at the trash's topmost position regardless of kind" (03 § Trash): the
/// ladder the rank is minted against is the whole container, cards and lane rows alike.
/// "Every arrival lands at the trash's topmost position regardless of kind" (03 § Trash)
/// and since 2026-07-31 that is the **merged `modified` order** doing it, over both kinds at
/// once, rather than a ladder the store minted against.
@Test("The lane lands at the top of the trash, above every existing entry")
func laneLandsOnTop() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let topBefore = try #require(loaded(fixture).trash.map(\.order).min())
store.delete([lane3])
let landed = try #require(loaded(fixture).trashedLanes.first?.order)
#expect(landed < topBefore)
#expect(try loaded(fixture).trashEntries.first?.id == lane3, "topmost row of the whole column")
// The lane's strip rank rode along untouched, which is what its restore reads.
#expect(try document(fixture, ".trash/\(Ident.lane3)").order == .valid(3072))
// And the next card delete mints above *that* the lane row is in the ladder the store
// mints against, which is the whole container rather than one array of it. The reload is
// what puts the new row in the snapshot; without it the store is still holding the
// pre-delete picture, as it is for two of any deletes in a row.
// And a *later* card delete lands above it the merged order is the whole container, so a
// row of the other kind is exactly as sortable as one of its own. The reload is what puts
// the new row in the snapshot; without it the store is still holding the pre-delete picture,
// as it is for two of any deletes in a row. The one-second sleep is load-bearing: `modified`
// serializes at whole-second granularity, so without it the two deletes share an instant and
// the deterministic title tail not recency would decide.
await reload(store)
try await Task.sleep(for: .seconds(1.1))
store.delete([card1])
let card = try #require(loaded(fixture).trash.first { $0.id == card1 }?.order)
#expect(card < landed)
#expect(try loaded(fixture).trashEntries.map(\.id).prefix(2) == [card1, lane3])
}
/// No dialog: "the move is recoverable, so nothing needs confirming" (03 § Trash).
@@ -544,11 +570,19 @@ private func makeTrashedLaneBoard() throws -> WriterFixture {
try fixture.item("", Item.board)
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First"))
try fixture.item(".trash/\(More.newer)", trashResident(order: "512", title: "Newer"))
try fixture.item(".trash/\(Ident.lane2)", trashedLane(order: "768", title: "Doing"))
// The column reads newest-first by `modified`: Newer, Doing (the lane row), Trashed the two
// kinds interleaved by the one stamp, which is the merged order this suite navigates by.
try fixture.item(
".trash/\(More.newer)",
trashResident(order: "512", title: "Newer", modified: "2026-05-03T09:00:00Z"))
try fixture.item(
".trash/\(Ident.lane2)",
trashedLane(order: "768", title: "Doing", modified: "2026-05-02T09:00:00Z"))
try fixture.item(".trash/\(Ident.lane2)/\(Ident.card3)", Item.rich(order: "1024", title: "Third"))
try fixture.item(".trash/\(Ident.lane2)/\(Ident.card4)", Item.rich(order: "2048", title: "Fourth"))
try fixture.item(".trash/\(Ident.indexless)", trashResident(order: "1024", title: "Trashed"))
try fixture.item(
".trash/\(Ident.indexless)",
trashResident(order: "1024", title: "Trashed", modified: "2026-05-01T09:00:00Z"))
return fixture
}
@@ -618,10 +652,9 @@ struct PurgeTrashedLaneTests {
#expect(!fixture.exists(".trash/\(Ident.lane2)"))
}
/// **The interim successor** (`SelectionGrammar.successor`'s trash branch): 04 settles navigation
/// and extension for the column's two kinds but not the successor, so it follows *navigation*
/// the next row down whatever its kind rather than stranding the selection. Gap card
/// 7b5cbc90 tracks the ruling; this test is the interim's marker as much as its cover.
/// **The successor is kind-blind** (04-interactions.md The map, ruled 2026-07-31, ratifying
/// what stood here as an interim): "the next row of either kind, in the same all-rows order plain
/// arrows walk repeated empties a mixed trash without dead-ends".
@Test("The successor after purging a lane row is the next row down, kind notwithstanding")
func successorCrossesKinds() throws {
let fixture = try makeTrashedLaneBoard()
@@ -764,21 +797,30 @@ struct StoreTombstoneMigrationTests {
#expect(try loaded(fixture).lanes.map(\.id.rawValue).contains(Ident.lane2))
}
@Test("Cards migrate oldest-first, so the newest deletion ends up on top")
func migrationOrderIsOldestFirst() throws {
/// **The stamps do the ordering, not the batch** (01 § Deletion, re-ruled 2026-07-31): each
/// migrated card takes its own `deleted:` timestamp as its `modified`, so the board's real
/// deletion order survives whatever sequence the heal happens to run in.
@Test("Migrated cards keep their real deletion order — newest deletion on top")
func migrationKeepsRealDeletionOrder() throws {
let fixture = try makeLegacyBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
store.migrateLegacyTombstones()
// Every arrival mints above the current top, so migrating oldest-first reproduces the
// newest-first column the tombstone model's timestamp sort used to render.
// 03-05 above 03-01, straight off the retired key not off the order the batch ran in.
#expect(try loaded(fixture).trash.compactMap(\.title.value) == ["Newer", "Older"])
let newerStamp = try #require(try document(fixture, ".trash/\(Ident.card3)").modified.value)
#expect(newerStamp == Date(timeIntervalSince1970: 1_772_701_200),
"2026-03-05T09:00:00Z — the legacy stamp, carried over verbatim")
}
@Test("The order is deterministic when the stamps are missing or unparseable")
func undatedSortsOldest() throws {
/// An **unparseable** legacy stamp is no evidence of when the card was deleted, so the migration
/// stamps it at migration time (01 § Deletion: "and from migration time otherwise") which
/// lands it among the freshest rather than inventing a date for it. Deterministic either way,
/// which is what this pins.
@Test("A card whose legacy stamp cannot be read is migrated at migration time")
func unparseableStampMigratesAtNow() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
@@ -795,9 +837,8 @@ struct StoreTombstoneMigrationTests {
store.migrateLegacyTombstones()
// "A corrupt stamp must not outrank fresh deletions for the trash's most prominent rows":
// undated sorts oldest, so it migrates first and ends up *below* the dated one.
#expect(try loaded(fixture).trash.compactMap(\.title.value) == ["Dated", "Corrupt"])
// Migration time is today, which is newer than any legacy stamp a real board carries.
#expect(try loaded(fixture).trash.compactMap(\.title.value) == ["Corrupt", "Dated"])
}
@Test("A board-level deleted: is never migrated — it is meaningless, ignored and logged")