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
+18 -8
View File
@@ -317,8 +317,10 @@ public enum BoardLoader: Sendable {
var trash: [Card] = []
var trashedLanes: [TrashedLane] = []
/// Every trash entry as the dedupe needs it, kind-blind the container is one flat list to
/// the identity rule, whatever the snapshot splits it into.
var trashEntries: [(id: ItemID, title: String?, order: Double)] = []
/// the identity rule, whatever the snapshot splits it into. Carried with the two keys the
/// container's order is stated in (`Ranks.isOrderedForTrash`), never `order`: the trash is
/// sorted by `modified` descending since 2026-07-31.
var trashEntries: [(id: ItemID, title: String?, modified: Date?)] = []
var trashKinds: [ItemID: IntegrityRules.ObjectKind] = [:]
for entryURL in trashCandidates(in: boardRoot) {
let entryName = entryURL.lastPathComponent
@@ -360,7 +362,7 @@ public enum BoardLoader: Sendable {
)
let id = ItemID(rawValue: entryName)
trashKinds[id] = kind
trashEntries.append((id: id, title: document.title.value, order: order))
trashEntries.append((id: id, title: document.title.value, modified: document.modified.value))
switch kind {
case .lane:
@@ -372,6 +374,7 @@ public enum BoardLoader: Sendable {
id: id,
schema: schema,
title: document.title,
modified: document.modified,
order: order,
heldCards: children().count,
document: document
@@ -407,12 +410,19 @@ public enum BoardLoader: Sendable {
// Display order is settled here rather than in the `BoardModel` call below, because the
// dedupe's last tie-break *is* traversal order and the rule needs the occurrences in it.
let orderedLanes = Ranks.sortedForDisplay(walkedLanes, order: \.order, name: \.name)
let orderedTrash = Ranks.sortedForDisplay(trash, order: \.order, name: { $0.id.rawValue })
let orderedTrashedLanes = Ranks.sortedForDisplay(trashedLanes, order: \.order, name: { $0.id.rawValue })
// The trash's own display order, **both kinds at once** the column interleaves them by rank
// **The trash's arrays are sorted by `modified` descending**, not by `order` (01-storage-format.md
// § Deletion, re-ruled 2026-07-31): the trash move rewrites no rank, so the stamp *is* the
// position. Each kind is sorted by the same comparator the merged `BoardModel.trashEntries`
// applies, which is what makes a kind-narrowed slice of the column agree with the column.
let orderedTrash = Ranks.sortedForTrash(
trash, modified: { $0.modified.value }, title: { $0.title.value }, name: { $0.id.rawValue })
let orderedTrashedLanes = Ranks.sortedForTrash(
trashedLanes, modified: { $0.modified.value }, title: { $0.title.value }, name: { $0.id.rawValue })
// The trash's own display order, **both kinds at once** the column interleaves them
// (03-board-ui.md § Trash), and the dedupe's last tie-break is stated in traversal order, so
// the two kinds are merged before the rule sees them rather than after.
let orderedTrashEntries = Ranks.sortedForDisplay(trashEntries, order: \.order, name: { $0.id.rawValue })
let orderedTrashEntries = Ranks.sortedForTrash(
trashEntries, modified: \.modified, title: \.title, name: { $0.id.rawValue })
let verdict = dedupeIdentities(
inBoardAt: boardRoot,
lanes: orderedLanes,
@@ -496,7 +506,7 @@ public enum BoardLoader: Sendable {
private static func dedupeIdentities(
inBoardAt root: URL,
lanes: [WalkedLane],
trash: [(id: ItemID, title: String?, order: Double)],
trash: [(id: ItemID, title: String?, modified: Date?)],
historyRanker: IdentityHistoryRanker?
) -> IntegrityRules.DedupeVerdict {
typealias Container = IntegrityRules.IdentityOccurrence.Container