Realign code with the 2026-07-29 findings-resolution rulings
Nine rulings land as code. Reorders don't stamp — one container-change predicate (WriteOperation.rewritesOrderOnly): within-container reorders and the renumber rescale rewrite only order, while cross-lane, cross-board, and trash moves stamp modified and clear modified-by; no trash special case exists, and the m8 undo inverses conform through the same seam. Copies are transactions: the root-strict/nested-lenient split retires for a whole-subtree stampability preflight that refuses loudly naming the offender, and every item-level copy severs remote/remote-state at every level (whole-board forks carry them verbatim). Paste refuses, never degrades: the embedded-index.md materialization and its loss row retire; a missing staged snapshot produces nothing and posts an error-tone one-shot named from manifest metadata. Coerce-tier fallbacks log through the Defect stream with path context attached loader-side. Displacement is level-uniform: a file squatting attachments inside a card heals by the same rename ladder as board-root squatters; comments stays tolerated. Delete Immediately joins card and lane context menus as Delete's ⌥-alternate with its own VO custom action, routed through an explicit container so the menu target outranks standing selection. Agent guide v7 teaches the stamp discipline and the card-level attachments claim, and sheds two stale v6 lines (lanes trash now; kind is taught). Verified conformant, unchanged: edition-aware Undo/Redo disable, trash marquee full-height backdrop. Both schemes 1854 tests / 318 suites green; verify-editions 30/30. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -37,8 +37,8 @@ public struct OneShotBanner: Identifiable, Sendable, Equatable {
|
||||
}
|
||||
|
||||
/// A loss row: content that didn't arrive though nothing failed (02-architecture.md § The banner
|
||||
/// surface, "Loss rows are the warning-tone class for non-failure losses", settled 2026-07-28) — a
|
||||
/// degraded paste, folders skipped from a Finder drop, their future kin.
|
||||
/// surface, "Loss rows are the warning-tone class for non-failure losses", settled 2026-07-28) —
|
||||
/// folders skipped from a Finder drop, the app's own relocation and repair notices, their future kin.
|
||||
///
|
||||
/// **It takes the one-shot's lifecycle**, deliberately: "a loss the user didn't notice is the harm,
|
||||
/// so it never auto-expires" is `OneShotBanner`'s "an error never evaporates unread", read for a row
|
||||
@@ -173,8 +173,8 @@ public enum BannerRow: Identifiable, Sendable {
|
||||
case reloadBreakage(BoardLoadError)
|
||||
/// A write that did not happen. Dismissable, error tone.
|
||||
case oneShot(OneShotBanner)
|
||||
/// Content that didn't arrive though nothing failed — a degraded paste, folders skipped from a
|
||||
/// Finder drop, their future kin. Dismissable, warning tone: below the true failures above it,
|
||||
/// Content that didn't arrive though nothing failed — folders skipped from a Finder drop, the
|
||||
/// app's own relocation and repair notices. Dismissable, warning tone: below the true failures above it,
|
||||
/// above the ambient notices below it (settled 2026-07-28, see `LossBanner`).
|
||||
case loss(LossBanner)
|
||||
/// History has stopped advancing. Condition, warning tone — the files are safe, only the undo
|
||||
@@ -392,43 +392,36 @@ public final class BannerCenter {
|
||||
postSignpost(Self.skippedStepMessage(direction, subject: subject))
|
||||
}
|
||||
|
||||
/// One item a degraded paste could not bring its attachments with — what
|
||||
/// `degradedPasteMessage(for:)` names.
|
||||
/// **The refused paste** (04-interactions.md ▸ Clipboard, re-ruled 2026-07-29): the staged
|
||||
/// snapshot was missing or unreadable, so the paste produced **nothing**, and this is the row that
|
||||
/// says so — "the paste produces nothing, and a one-shot failure banner names it from the
|
||||
/// manifest's metadata".
|
||||
///
|
||||
/// `title` is the item's as written, `nil` for an untitled one: "Untitled" is a rendering, never
|
||||
/// a value (03-board-ui.md § Card face), and the phrasing below says "the item" instead, exactly
|
||||
/// as `actionPhrase(for:)` does for a failure whose title never got read.
|
||||
public struct AttachmentLoss: Sendable, Equatable {
|
||||
public let title: String?
|
||||
public let attachments: Int
|
||||
|
||||
public init(title: String?, attachments: Int) {
|
||||
self.title = title
|
||||
self.attachments = attachments
|
||||
}
|
||||
}
|
||||
|
||||
/// **The degraded paste** (04-interactions.md ▸ Clipboard, settled): the staged snapshot was
|
||||
/// missing or unreadable, so the paste fell back to the manifest's embedded `index.md` — content
|
||||
/// intact, attachments absent — and this is the row that says so. "A degraded paste is loud,
|
||||
/// never silent … the user never discovers an empty `attachments/` later."
|
||||
/// **A `oneShot`, not a loss row** — which is the pivot, and it is the vocabulary reading the
|
||||
/// event correctly rather than a reclassification for its own sake. The degraded paste *was* a loss
|
||||
/// row because the items landed and only their attachments did not: content that didn't arrive
|
||||
/// though nothing failed. Under refuse-don't-degrade nothing lands at all, which is exactly
|
||||
/// 02-architecture.md's definition of a one-shot — a write that did not happen — so the row
|
||||
/// carries a `BoardWriteError` like every other failure, ranks with the true failures, and says
|
||||
/// "Couldn't paste" rather than "Pasted … without".
|
||||
///
|
||||
/// **A loss row, not a `oneShot` and not a signpost** — the vocabulary's answer rather than a
|
||||
/// compromise (settled 2026-07-28). 02-architecture.md's `oneShot` is *a write that did not
|
||||
/// happen*, carrying a `BoardWriteError`, and nothing here failed — the items landed, whole but
|
||||
/// for files that were never on the pasteboard's side of the transfer. This row first shipped as
|
||||
/// a signpost, the vocabulary's other one-shot-lifecycle member at the time, and it read quieter
|
||||
/// than 04's "loud" deserved: a signpost ranks last and may collapse behind "+N more", exactly
|
||||
/// where a board already showing real trouble would bury it. The loss class exists to close that
|
||||
/// gap — content that didn't arrive though nothing failed ranks below the true failures and
|
||||
/// above the ambient notices, keeping the signpost's dismissable, untimed lifecycle without
|
||||
/// inheriting its bottom-of-the-strip precedence.
|
||||
/// The retired member is `postDegradedPaste(_:)` and its `AttachmentLoss` payload: with the
|
||||
/// degraded materialization gone there is no partial arrival to account for, and the
|
||||
/// loss-accounting problem it existed to report — what didn't arrive, and whether the totals were
|
||||
/// honest — dissolves rather than being solved. **The loss class itself is untouched**: folder-drop
|
||||
/// skips still post one (`postSkippedFolders`), and the relocation, migration, displacement and
|
||||
/// remint notices are all still its.
|
||||
///
|
||||
/// An empty list posts nothing: a fallback that lost no attachments lost nothing at all, and a
|
||||
/// banner announcing that would be noise.
|
||||
public func postDegradedPaste(_ losses: [AttachmentLoss]) {
|
||||
guard let message = Self.degradedPasteMessage(for: losses) else { return }
|
||||
postLoss(message)
|
||||
/// `title` is the offending entry's, from the manifest's own metadata, `nil` for an untitled item —
|
||||
/// "Untitled" is a rendering, never a value (03-board-ui.md § Card face), so `actionPhrase(for:)`
|
||||
/// says "the item" instead. The staging path is what the error names as its `path`: it is the file
|
||||
/// that was not there, and naming it is what makes a bug report about this actionable.
|
||||
public func postRefusedPaste(title: String?, stagedAt path: String) {
|
||||
post(BoardWriteError(
|
||||
operation: .paste(title: title),
|
||||
path: path,
|
||||
reason: .clipboardContentGone
|
||||
))
|
||||
}
|
||||
|
||||
/// One card whose loose files were relocated into `attachments/` — what
|
||||
@@ -547,9 +540,10 @@ public final class BannerCenter {
|
||||
/// 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
|
||||
/// banner names the skipped folders" — now a loss row, for the same reason the degraded paste is
|
||||
/// one (settled 2026-07-28): folders that never arrived are a non-failure loss, not a write
|
||||
/// failure.
|
||||
/// banner names the skipped folders" — now a loss row (settled 2026-07-28): folders that never
|
||||
/// arrived are a non-failure loss, not a write failure. With the degraded paste retired
|
||||
/// (`postRefusedPaste`) this is the loss class's clearest remaining instance: the drop *did* land,
|
||||
/// and only the payload the attachment model cannot hold stayed behind.
|
||||
///
|
||||
/// A drop with no skipped folders posts nothing — nothing was lost, so there is nothing to say.
|
||||
public func postSkippedFolders(count: Int) {
|
||||
@@ -653,7 +647,7 @@ public final class BannerCenter {
|
||||
///
|
||||
/// `signposts` carries a default because its producer is m6's card window and nothing posts one
|
||||
/// today; every other class has a live producer and is spelled out at every call site — `losses`
|
||||
/// included, since a degraded paste already posts one (`postDegradedPaste`).
|
||||
/// included, since a Finder drop that skipped folders already posts one (`postSkippedFolders`).
|
||||
public nonisolated static func rows(
|
||||
lock: ReadOnlyLockReason?,
|
||||
breakage: BoardLoadError?,
|
||||
@@ -756,6 +750,13 @@ public final class BannerCenter {
|
||||
if let title { "Couldn't reorder '\(title)'" } else { "Couldn't reorder the item" }
|
||||
case let .copy(title):
|
||||
if let title { "Couldn't copy '\(title)'" } else { "Couldn't copy the item" }
|
||||
case let .paste(title):
|
||||
// **The command's own verb** (04-interactions.md ▸ Clipboard's own example sentence,
|
||||
// "Couldn't paste 'Fix login' — the copied content is gone"). The user pressed ⌘V, and
|
||||
// "Couldn't copy" — the operation the Writer would have run — would name a gesture they
|
||||
// never made. The item is named from the manifest's metadata, which is what the embedded
|
||||
// `index.md` is kept for now that it is never a materialization source.
|
||||
if let title { "Couldn't paste '\(title)'" } else { "Couldn't paste the item" }
|
||||
case let .delete(title):
|
||||
if let title { "Couldn't delete '\(title)'" } else { "Couldn't delete the item" }
|
||||
case let .purge(title):
|
||||
@@ -865,6 +866,13 @@ public final class BannerCenter {
|
||||
// outlet raises this in its alert and never here — the store validates before it opens a
|
||||
// write bracket — so this line exists for a caller that reached the Writer directly.
|
||||
error.reason.description
|
||||
case .clipboardContentGone:
|
||||
// **04's own words** ("the copied content is gone"), and the whole of what can honestly be
|
||||
// said: the snapshot the pasteboard promised is not on disk, so there is no cause to
|
||||
// diagnose past that. It deliberately says nothing about *why* — a sweep that ran early, a
|
||||
// container the system reclaimed, an unmounted volume — because the user's recovery is the
|
||||
// same in every case, and 04 names it: ⌘C again.
|
||||
"the copied content is gone"
|
||||
}
|
||||
return trimmed(text)
|
||||
}
|
||||
@@ -904,32 +912,6 @@ public final class BannerCenter {
|
||||
return "\(subject): \(reason) — showing the last good view"
|
||||
}
|
||||
|
||||
/// The degraded paste's line — 04-interactions.md's own example sentence, "Pasted 'Fix login'
|
||||
/// without its 3 attachments", generalized over the two axes it can vary on.
|
||||
///
|
||||
/// **It names exactly what was lost**, which is what the design asks for and what decides every
|
||||
/// choice below: the count is real (never "some"), the singular and the plural are both spelled,
|
||||
/// and a multi-item paste totals the attachments rather than listing every title — a banner is one
|
||||
/// line, and "2 items" plus the true total is the honest summary where a truncated list would not
|
||||
/// be. `nil` for an empty list: nothing was lost, so there is nothing to say.
|
||||
///
|
||||
/// The count is the item's `attachments/` as the snapshot listed it at copy time — the design's
|
||||
/// own vocabulary for what a card carries (01-storage-format.md § Attachments). A stray file
|
||||
/// sitting loose in the card folder is not in it and is not named here; see the report's
|
||||
/// design-gap note.
|
||||
public nonisolated static func degradedPasteMessage(for losses: [AttachmentLoss]) -> String? {
|
||||
guard !losses.isEmpty else { return nil }
|
||||
let total = losses.reduce(0) { $0 + $1.attachments }
|
||||
guard total > 0 else { return nil }
|
||||
|
||||
guard losses.count == 1, let only = losses.first else {
|
||||
return "Pasted \(losses.count) items without their \(total) attachments"
|
||||
}
|
||||
let subject = only.title.map { "'\($0)'" } ?? "the item"
|
||||
let tail = total == 1 ? "its attachment" : "its \(total) attachments"
|
||||
return "Pasted \(subject) without \(tail)"
|
||||
}
|
||||
|
||||
/// The skipped-folders line — 04-interactions.md's own example, "Folders can't be attached — 2
|
||||
/// skipped", generalized over the count. `postSkippedFolders` never calls this at `count == 0`,
|
||||
/// so every real call already has something to report.
|
||||
@@ -954,9 +936,8 @@ public final class BannerCenter {
|
||||
/// a list of names would be the first thing to truncate; the card is still named, which is
|
||||
/// what makes the notice actionable — the user knows exactly which `attachments/` to look in.
|
||||
/// - **Several cards** folds again, to two counts: "Moved 5 files into attachments — 3 cards"
|
||||
/// (settled here, the judgment 01 leaves to the implementation). It is the degraded paste's
|
||||
/// own shape — "Pasted 2 items without their 5 files" — and for its reason: the true total
|
||||
/// plus the true item count is the honest summary where a truncated list of titles would not
|
||||
/// (settled here, the judgment 01 leaves to the implementation): the true total plus the true
|
||||
/// item count is the honest summary where a truncated list of titles would not
|
||||
/// be. This case is the whole-board sweep (a board opened after an agent scattered files
|
||||
/// across it), where naming three cards of eleven would read as a bug.
|
||||
///
|
||||
|
||||
@@ -2495,21 +2495,25 @@ public final class BoardStore: HealHost {
|
||||
|
||||
/// Where one arriving item's bytes come from.
|
||||
///
|
||||
/// **Two producers, one arrival path.** A drag names folders in the source board; a paste names
|
||||
/// folders in the clipboard's staging directory — and, when that snapshot is missing or
|
||||
/// unreadable, the manifest's embedded `index.md` instead (04-interactions.md ▸ Clipboard's
|
||||
/// staging-less fallback). Modelling the fallback as a second kind of *source* rather than as a
|
||||
/// second arrival method is what keeps the rank insertion, the tombstone stripping and the
|
||||
/// `deleted:` clearing stated once: everything downstream of "where do the bytes come from" is
|
||||
/// identical, and a paste that half-falls-back mixes the two cases inside one bracket.
|
||||
/// **One producer shape, because there is only one kind of source left.** A drag names folders in
|
||||
/// the source board; a paste names folders in the clipboard's staging directory. There used to be a
|
||||
/// second case — the manifest's embedded `index.md`, materialized when the staged snapshot was
|
||||
/// missing or unreadable — and it is **retired with the degraded paste** (04-interactions.md ▸
|
||||
/// Clipboard, re-ruled 2026-07-29: "A paste whose staged snapshot is missing or unreadable refuses
|
||||
/// loudly — never degrades"). An item arrives **whole — index, attachments, loose files — or not at
|
||||
/// all**, so a paste can no longer half-fall-back inside one bracket, and the loss-accounting
|
||||
/// problem the second case created dissolves rather than being solved. The manifest still embeds
|
||||
/// `index.md`, now purely as identification metadata: menu validation, the refusal's wording, and
|
||||
/// the plain-text flavor.
|
||||
///
|
||||
/// It stays a single-case enum rather than collapsing to a bare `URL`: the arrival paths read as
|
||||
/// "where do these bytes come from", the drag and the clipboard each say so at their own call
|
||||
/// sites, and a future third producer (an import, a drop from another document type) has a place to
|
||||
/// land that is not a rewrite of every signature in between.
|
||||
public enum ItemSource: Sendable, Equatable {
|
||||
|
||||
/// A folder on disk — the source board's own, or a staged snapshot of it.
|
||||
case folder(URL)
|
||||
|
||||
/// The manifest's embedded text: the item's `index.md`, and (for a lane) its cards'.
|
||||
/// Materialized by `BoardWriter.materializeItem`, byte-faithfully.
|
||||
case text(index: String, cards: [String])
|
||||
}
|
||||
|
||||
/// A cross-board card drop, landing contiguously at `index` among `laneID`'s rendered cards.
|
||||
@@ -2535,10 +2539,11 @@ public final class BoardStore: HealHost {
|
||||
/// independently (04-interactions.md ▸ Clipboard).
|
||||
///
|
||||
/// It is the same commit as a drop's, deliberately: `.copy` materializes from the staged snapshot
|
||||
/// (or, per entry, from the embedded `index.md`) and `.move` is the armed cut's — "the ⌘-drag
|
||||
/// move path — identity travels", which is also the keyboard restore when the cut was made in the
|
||||
/// trash (▸ The trash: "cut in the trash, paste into a lane is the keyboard-native restore, an
|
||||
/// ordinary folder move").
|
||||
/// and `.move` is the armed cut's — "the ⌘-drag move path — identity travels", which is also the
|
||||
/// keyboard restore when the cut was made in the trash (▸ The trash: "cut in the trash, paste into a
|
||||
/// lane is the keyboard-native restore, an ordinary folder move"). A copy whose staged snapshot is
|
||||
/// missing never reaches here at all: the clipboard refuses the whole paste in front of this call
|
||||
/// (04 ▸ Clipboard, re-ruled 2026-07-29), so every source this sees is a folder that exists.
|
||||
///
|
||||
/// **The trash's old copy-out rule is gone with the tombstone it stripped** (resettled
|
||||
/// 2026-07-28): a trashed card carries no `deleted:` key, so a card copied out of the trash is
|
||||
@@ -2616,15 +2621,14 @@ public final class BoardStore: HealHost {
|
||||
}
|
||||
}
|
||||
|
||||
/// One arrival's materialization — the two `ItemSource` kinds crossed with the two operations,
|
||||
/// in the one place both the card path and the lane path can share.
|
||||
/// One arrival's materialization — the source crossed with the two operations, in the one place
|
||||
/// both the card path and the lane path can share.
|
||||
///
|
||||
/// **`.move` of a `.text` source is unreachable and answers `nil`.** A move needs a folder whose
|
||||
/// identity travels, and the only producer of text sources is the clipboard's fallback, which is
|
||||
/// a *copy* by construction (04-interactions.md ▸ Clipboard: an armed cut moves the surviving
|
||||
/// originals, and a cut that cannot find them is void). Skipping is the standing posture for an
|
||||
/// arrival that names nothing — the same silent no-op every other drop commit gives a source that
|
||||
/// has gone.
|
||||
/// A copy is `copyItem` (fresh GUIDs throughout, the copy contract applied to every folder it
|
||||
/// materializes) and a move is `moveItem` (identity travels, the import boundary reminting only
|
||||
/// what collides). The `ItemID?` return survives the retired text case because the lane and card
|
||||
/// arrival loops read it as "did this arrival land": a `nil` is the standing silent no-op for a
|
||||
/// source that names nothing.
|
||||
private static func materialize(
|
||||
_ source: ItemSource,
|
||||
operation: TransferOperation,
|
||||
@@ -2644,15 +2648,6 @@ public final class BoardStore: HealHost {
|
||||
destinationBoardRoot: destinationBoardRoot,
|
||||
order: order
|
||||
).id
|
||||
case let (.text(index, cards), .copy):
|
||||
return try BoardWriter.materializeItem(
|
||||
inParent: parent,
|
||||
indexText: index,
|
||||
children: cards,
|
||||
order: order
|
||||
)
|
||||
case (.text, .move):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2677,7 +2672,7 @@ public final class BoardStore: HealHost {
|
||||
)
|
||||
}
|
||||
|
||||
/// **The clipboard's lane arrival** — `receiveLanes` with the staging-less fallback folded in
|
||||
/// **The clipboard's lane arrival** — `receiveLanes` with the paste's own axis folded in
|
||||
/// (04-interactions.md ▸ Clipboard).
|
||||
///
|
||||
/// The two operations keep their drag semantics exactly, because 04 says they are the same
|
||||
@@ -3445,8 +3440,19 @@ public final class BoardStore: HealHost {
|
||||
/// The selection is cleared rather than walked to a successor: unlike ⌫, this is the command a
|
||||
/// confirmation stands in front of, and what follows it is reading the board rather than pressing
|
||||
/// the key again.
|
||||
public func deleteImmediately(_ ids: Set<ItemID>) {
|
||||
let container = selection.container
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - ids: the items to purge — the caller's explicit set, not this store's selection.
|
||||
/// - container: **which side those ids live on**, supplied rather than read off the selection
|
||||
/// (added with the context menu's ⌥-alternate — 11-command-nexus.md ▸ Context menus). The
|
||||
/// menu-bar caller passes the selection's own container and behaves exactly as before; a
|
||||
/// context-menu caller passes `.board`, because "the click names its target" and the target can
|
||||
/// legitimately sit on the other side of the container boundary from a standing selection
|
||||
/// (797d020's explicit-set resolution, whose whole point is that the two can disagree). Reading
|
||||
/// it off the selection here would silently resolve a board card against `.trash` and purge
|
||||
/// nothing — a confirmed destructive command turning into a no-op, which is the one outcome a
|
||||
/// confirmation must never lead to.
|
||||
public func deleteImmediately(_ ids: Set<ItemID>, in container: ItemContainer) {
|
||||
let paths = ItemPath.resolve(ids, in: container, snapshot: snapshot).filter { !$0.isLane }
|
||||
guard !paths.isEmpty else { return }
|
||||
let root = rootURL
|
||||
@@ -3604,19 +3610,28 @@ public final class BoardStore: HealHost {
|
||||
|
||||
// MARK: - The claimed-name displacement
|
||||
|
||||
/// Moves a squatter off a board-root name the app claims, and posts the notice naming old and
|
||||
/// new — the **act** half of the claimed-names ruling (01-storage-format.md § Fractal layout ▸
|
||||
/// Rules, ruled 2026-07-29: "Lanework owns the board, so an invalid artifact on a claimed name is
|
||||
/// a defect, not a resident").
|
||||
/// Moves squatters off the names the app claims, and posts the notice naming old and new — the
|
||||
/// **act** half of the claimed-names ruling (01-storage-format.md § Fractal layout ▸ Rules, ruled
|
||||
/// 2026-07-29: "Lanework owns the board, so an invalid artifact on a claimed name is a defect, not
|
||||
/// a resident").
|
||||
///
|
||||
/// Today that is `.trash` and only `.trash`: a file or symlink sitting on the name the trash
|
||||
/// needs, which breaks deletion for as long as it stands — which is exactly why the timing is
|
||||
/// *scheduled* rather than on-touch (§ Validation and healing: "proactive when the defect is
|
||||
/// load-bearing now"). `CLAUDE.md`'s squatter is displaced by the guide's own heal, which owns
|
||||
/// that file end to end.
|
||||
/// **Two levels, one heal** (extended 2026-07-29 — "the rule is level-uniform"): the board root's
|
||||
/// `.trash`, and any card's `attachments`. Each is a file or symlink sitting on a name the app needs
|
||||
/// — breaking deletion in the first case, and every import, Finder drop and sidebar listing for that
|
||||
/// card in the second — which is exactly why the timing is *scheduled* rather than on-touch
|
||||
/// (§ Validation and healing: "proactive when the defect is load-bearing now"). `CLAUDE.md`'s
|
||||
/// squatter is displaced by the guide's own heal, which owns that file end to end; a wrong-kinded
|
||||
/// `comments` is a tolerated stray until the feature consumes the name.
|
||||
///
|
||||
/// **Displacement, never destruction**, and never a mint: the freed name is left empty and the
|
||||
/// *next delete* creates the real `.trash/`, exactly as it does on a board that never had one.
|
||||
/// **One bracket over every displacement the load found**, whatever their levels: the batch is one
|
||||
/// app-mediated reload and, on git boards, one heal commit — the loose-file relocation's rule, and
|
||||
/// this heal's own memo is board-wide anyway.
|
||||
///
|
||||
/// **Displacement, never destruction**, and never a mint: the freed name is left empty and the next
|
||||
/// gesture that needs it creates the real folder — the next delete mints `.trash/`, the next import
|
||||
/// mints `attachments/` — exactly as on a board that never had one. The displaced file, now an
|
||||
/// ordinary loose file beside the card's `index.md`, is picked up by the next load's loose-file
|
||||
/// relocation and lands in the real `attachments/`: the heals compose, which is 01's own word for it.
|
||||
public func displaceClaimedNames() {
|
||||
let work = claimedNameSquatters
|
||||
let root = rootURL
|
||||
|
||||
Reference in New Issue
Block a user