Dedupe duplicate ids and heal them silently
The crash-class gap the integrity design pass found (DESIGN/01 - Fractal layout rules; 02 - Live-reload resilience): the loader had no board-wide dedupe at all, so two hand-copied folders sharing a UUID put two equal ItemIDs into one snapshot - which SwiftUI's ForEach does not tolerate. Built to the day's re-rulings, both landing mid-flight: the user-gated Repair banner retired (176c852- the heal runs silently) and the container boundary became the first tie-break (f153e79- the visible card never loses to its own trash ghost). IntegrityRules.dedupe (pure, occurrence list in, verdict out): group by canonical identity, collapse case-spelled twins first - spellings with a live occurrence outrank trash-only spellings, then canonical lowercase, then lexicographically first; losers are silent strays (LoadWarning.caseTwinIgnored - spelling artifacts, never reminted) - then earlier-occurrence-wins across the surviving spelling's folders on a four-rung ladder: live-before-trashed, git path history rank, FS birth date (nil is no comparison, never .distantPast), traversal order. Occurrences are exactly the identity-bearing folders: lanes, cards, .trash entries - a UUID-shaped folder under a card is content. BoardLoader walks lanes as WalkedLane and builds Lane values only on the far side of the verdict, so a withheld card can never reach a snapshot; a name-only gate keeps the healthy-board cost at one dictionary pass, no disk reads. Withheld subtrees are still walked - a hand-copied lane's nested cards are their own withheld occurrences, reminted at the finest grain like the import boundary would have. A withheld trash entry's trashKinds reading leaves with it. The git rung is a seam (BoardLoader.IdentityHistoryRanker, one closure keyed by root-relative path) because base links no git machinery - base injects nothing and falls through; pro-m1 owns the ranker (card annotated). The heal: Defect.duplicateIdentity (signature duplicate:<path>:<id>) rides HealScheduler as the fourth scheduled heal, ordered last among the content heals because a remint renames folders and would stale the paths the same load handed the relocation and migration. BoardWriter.remintDuplicateIdentity re-verifies twice at write time - the folder still carries the losing identity AND something else still does (the vanished-duplicate race no-ops from either side) - then renames to a fresh v4 minted against the whole board's identity bag. A rename and nothing else: no index.md opened, no modified stamp, no modified-by clear; the receipt is heal-marked (pro-m1's committer splits it out, named by 06's kept Repair verb); no undo step - heals are not gestures. The notice is the design's own sentence ("Repaired duplicate id - 'Fix login'"; several fold to a count), a loss row on the relocation's reasoning; WriteOperation.repairDuplicateID carries the failure mirror. Fixture repair rode along: duplicate-order-tie-break.kanban had a lane and its own card sharing a UUID - a genuine duplicate the new pass correctly withholds; the folder rename landed infeae6d0, the matching test constant lands here. 66 tests added (DuplicateIdentityTests: the ladder rung by rung, the straddles, withheld-lane subtrees, remint idempotence and races, the one-heal-cycle window, all phrasing). 1804 green on both schemes. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -524,6 +524,26 @@ public final class BannerCenter {
|
||||
postLoss(message)
|
||||
}
|
||||
|
||||
/// **The duplicate-id remint** (01-storage-format.md § Fractal layout ▸ Rules, re-ruled
|
||||
/// 2026-07-29): two folders were carrying one id, the app gave the later one the fresh identity a
|
||||
/// copy should have had, and this is the row that says so — "Announced, not invisible: the
|
||||
/// relocation-style warning-tone notice names the repair … identity changed, so a line says so,
|
||||
/// but nothing waits on consent".
|
||||
///
|
||||
/// **A loss row, on `postRelocatedLooseFiles`' exact reasoning**, and the ruling asks for that
|
||||
/// class by name: the app renamed a folder of the user's that they did not ask it to rename, so it
|
||||
/// must be said out loud, must not evaporate unread, and must not rank as an error, because
|
||||
/// nothing failed. It is emphatically **not** a condition banner with a button — the user-gated
|
||||
/// Repair retired on 2026-07-29 and this row is what replaced it.
|
||||
///
|
||||
/// `titles` are the reminted items' as the load found them, `nil` for an untitled one — "Untitled"
|
||||
/// is a rendering, never a value (03-board-ui.md § Card face). A remint that reminted nothing
|
||||
/// posts nothing.
|
||||
public func postRemintedDuplicateIDs(_ titles: [String?]) {
|
||||
guard let message = Self.remintedDuplicateIDsMessage(for: titles) else { return }
|
||||
postLoss(message)
|
||||
}
|
||||
|
||||
/// Posts the skipped-folders loss row for a Finder drop that imported its files but refused its
|
||||
/// folders (04-interactions.md ▸ Selection, drag & drop, "Folders are refused at hover"): "a
|
||||
/// mixed drag proposes for its files only, and the drop imports the files while a one-shot
|
||||
@@ -798,6 +818,13 @@ public final class BannerCenter {
|
||||
// held, the feature that needs it does not work, and the fix is theirs (move or rename
|
||||
// the thing sitting there) because the app has just demonstrated it cannot.
|
||||
"Couldn't move '\(name)' aside — Lanework needs that name"
|
||||
case let .repairDuplicateID(title):
|
||||
// **The failure's mirror of the success row** ("Repaired duplicate id — 'Fix login'"), in
|
||||
// the same words, so the two read as one sentence and its negation. It names the *defect*
|
||||
// rather than the mechanics ("couldn't rename a folder" would describe an act the user has
|
||||
// no model of) and stays graceful when the item is untitled, because the enum knows a
|
||||
// title and never a kind.
|
||||
if let title { "Couldn't repair the duplicate id of '\(title)'" } else { "Couldn't repair a duplicate id" }
|
||||
case let .toggleTask(title):
|
||||
// The user's word for it, not the file's: they ticked a box. The card is named where
|
||||
// the read that preceded the flip learned its title, so a body write that refused says
|
||||
@@ -1019,6 +1046,28 @@ public final class BannerCenter {
|
||||
return "Renamed '\(only.name)' to '\(only.movedTo)' — Lanework needs that name"
|
||||
}
|
||||
|
||||
/// The duplicate-id remint's line — **the design's own sentence**, verbatim
|
||||
/// (01-storage-format.md § Fractal layout ▸ Rules, re-ruled 2026-07-29: "posts 'Repaired duplicate
|
||||
/// id — 'Fix login''").
|
||||
///
|
||||
/// - **One**: "Repaired duplicate id — 'Fix login'". The relocation's idiom exactly — the act
|
||||
/// first, the subject after an em dash — and it names the *id* rather than the folder, because
|
||||
/// the folder name is a UUID no user has a relationship with. There is no explanatory tail: the
|
||||
/// act is its own explanation, and nothing about the user's content changed.
|
||||
/// - **Several**: folded to a count, the idiom's plural — "Repaired 3 duplicate ids". Several
|
||||
/// arrive together routinely (a hand-copied lane's cards all collide at once), and a sentence
|
||||
/// naming each would be longer than the row.
|
||||
///
|
||||
/// `nil` when nothing was reminted — a heal that healed nothing is not news, which is also the
|
||||
/// vanished-duplicate race's whole outward appearance: silence.
|
||||
public nonisolated static func remintedDuplicateIDsMessage(for titles: [String?]) -> String? {
|
||||
guard let only = titles.first else { return nil }
|
||||
guard titles.count == 1 else {
|
||||
return "Repaired \(titles.count) duplicate ids"
|
||||
}
|
||||
return "Repaired duplicate id — \(sole(only))"
|
||||
}
|
||||
|
||||
/// A sole migrated item's name: its title in quotes, or the untitled rendering the relocation
|
||||
/// line already uses ("an untitled card" / "an untitled lane" are one phrase here, because the
|
||||
/// clause it sits in already says which level it is).
|
||||
|
||||
Reference in New Issue
Block a user