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:
2026-07-30 06:49:11 -04:00
parent 5ae48de0ea
commit 69084fdff7
27 changed files with 2159 additions and 542 deletions
+274 -115
View File
@@ -17,8 +17,10 @@ import Foundation
/// and the body survive every write by construction rather than by remembering to preserve
/// them.
/// - **`modified` stamped, `modified-by` cleared** (§ Frontmatter): on every app-mediated write
/// path. Absence of `modified-by` means "the board's user, via the app"; the file is being
/// rewritten anyway, so clearing an external writer's self-reported stamp costs nothing.
/// path that rewrites *content*. Absence of `modified-by` means "the board's user, via the app";
/// the file is being rewritten anyway, so clearing an external writer's self-reported stamp costs
/// nothing. The one class of write that does neither is the **order-only rewrite**
/// (`WriteOperation.rewritesOrderOnly` the reorders-don't-stamp rule).
/// - **Encoding** (§ Fractal layout Rules): writes are BOM-less UTF-8; reads are strict
/// UTF-8, and a file that does not decode is a loud, specific error rather than a
/// lossy best guess.
@@ -44,6 +46,26 @@ public enum BoardWriter: Sendable {
/// two keys, and no call site has to remember them.
/// 4. **Atomic replace.**
///
/// ## The reorders-don't-stamp predicate
///
/// Step 3 is **skipped for an order-only rewrite** (01-storage-format.md § Frontmatter
/// `modified`'s scope ruled 2026-07-29 as moves-don't-stamp, refined 2026-07-30 to the
/// container-change predicate). `order` is logically the *container's* property a relationship
/// among a lane's members that the format happens to store inside each member's file so a
/// rewrite that only restates position touches no content: no `modified` stamp, and no
/// `modified-by` clear (the two are paired; attribution cannot change when content didn't).
///
/// **The predicate is the operation's, not a parameter** (`WriteOperation.rewritesOrderOnly`):
/// the vocabulary already draws the line this rule needs `.reorder` is by construction the
/// same-container case (`moveItem` decides it from the two URLs before it touches disk) and
/// `.renumberChildren` is the whole-lane rescale. Deriving it here rather than asking each call
/// site means no caller can forget, and the rule stays one exhaustive switch a suite can pin
/// without a filesystem.
///
/// **There is no trash branch anywhere**, deliberately: a move into or out of `.trash/` changes
/// the item's container, so it stamps for the same reason a cross-lane move does. The trash move
/// is the container rule's plainest instance rather than an exception to a rule about moves.
///
/// The **one path that deliberately bypasses this** is the card window's raw-source Apply
/// (05-card-window.md): it writes the user's text byte-for-byte and does *not* clear a
/// `modified-by` the user typed or kept the validated-then-verbatim contract outranks the
@@ -90,8 +112,13 @@ public enum BoardWriter: Sendable {
// After `edits`, so a caller that wrote its own `kind` is left alone, and before the stamps,
// which outrank everything for their own reason.
IntegrityRules.healOnTouch(&document, kind: kind ?? derivedKind(ofItemFolder: folder))
document.set(FrontmatterKeys.modified, to: .date(Date()))
document.remove(FrontmatterKeys.modifiedBy)
// The reorders-don't-stamp predicate, read off the operation. An order-only rewrite restates
// the container's own arrangement and leaves both provenance keys exactly as it found them
// a standing `modified-by` survives a reorder, which is the pairing 01 spells out.
if !operation.rewritesOrderOnly {
document.set(FrontmatterKeys.modified, to: .date(Date()))
document.remove(FrontmatterKeys.modifiedBy)
}
try atomicReplace(text: document.serialized(), at: indexURL, operation: operation)
}
@@ -403,6 +430,12 @@ public enum BoardWriter: Sendable {
/// - **Display order is the assignment order** (`Ranks.isOrderedForDisplay`: `order`
/// ascending, folder name breaking ties) the same rule the loader sorts by, so a
/// renumber is guaranteed to be sequence-preserving: nothing visibly moves.
/// - **Nothing is stamped.** A rescale is order-only, so no sibling's `modified` moves and no
/// sibling's `modified-by` is cleared (01-storage-format.md § Ordering, verbatim: "order-only
/// rewrites, so no `modified` stamp and no `modified-by` clear"). That falls out of
/// `.renumberChildren` answering `rewritesOrderOnly` rather than being arranged here which is
/// what keeps a whole lane's worth of bookkeeping from looking like a whole lane's worth of
/// edits to the card window, to a future auto-purge, and to an agent's own attribution.
///
/// Each child's rewrite is atomic; the batch is not. An interrupted renumber leaves some
/// siblings renumbered and some not every `order` still a valid float, display order
@@ -498,10 +531,17 @@ public enum BoardWriter: Sendable {
/// construction rather than by copying carefully.
///
/// Exactly one file is rewritten the moved root's `index.md`, and only to carry its new
/// `order` (§ Ordering, "a reorder rewrites only the moved item's `index.md`"), stamped and
/// `modified-by`-cleared like every other app write. `order` is the caller's explicit rank
/// (a drop between two siblings), or `nil` to append after the destination's visible
/// siblings.
/// `order` (§ Ordering, "a reorder rewrites only the moved item's `index.md`"). `order` is the
/// caller's explicit rank (a drop between two siblings), or `nil` to append after the
/// destination's visible siblings.
///
/// **Whether that rewrite stamps is the container question** (§ Frontmatter `modified`'s scope,
/// refined 2026-07-30 `WriteOperation.rewritesOrderOnly`), and this call is where it is
/// answered for every move in the app: the same-parent degenerate path below is a `.reorder` and
/// rewrites `order` alone, while a real move cross-lane, cross-board, into or out of `.trash/`
/// is a `.move` and stamps `modified` and clears `modified-by` like any content write. The
/// branch that already exists for the *rank arithmetic* is therefore the whole of the stamping
/// rule too; there is no second test, and pointedly no trash case.
///
/// **The import boundary is the one place within-board uniqueness is enforced** (§ Fractal
/// layout Rules). `sourceBoardRoot` and `destinationBoardRoot` are compared by resolved,
@@ -762,23 +802,36 @@ public enum BoardWriter: Sendable {
/// `Date` for the whole tree: a card made from a template is born today, not forked from
/// the template (09-templates.md).
///
/// Two deliberate leniencies below the root, both of them "what a hand copy would do":
/// Plus the tracker sever, which is the copy contract's third clause: every folder this
/// materializes drops the reserved `remote`/`remote-state` keys, at every level
/// (`applyCopyContract`).
///
/// - A nested `index.md` that is **readable-but-uneditable** (§ Frontmatter), or that cannot
/// be read at all, is copied byte-verbatim and simply not stamped. Refusing an entire copy
/// because one nested card is a flow mapping would be hostile, and the file arrives
/// *exactly* as it was rather than corrupted its stale `modified-by` attribution
/// surviving is the self-reported-provenance honest limit § Frontmatter already
/// acknowledges. The **root** gets no such leniency: it must be rewritten (it needs its
/// new `order`), so an unreadable or uneditable root refuses the copy up front, before
/// anything is materialized.
/// - A nested UUID-shaped folder with **no `index.md`** interrupted-create residue is
/// copied and reminted like any other, and not rewritten: the same skip the loader applies
/// to it (`.missingIndex`).
/// ## A copy is a transaction (ruled 2026-07-29)
///
/// **The whole subtree is preflighted for stampability before anything is materialized**, and a
/// copy that cannot honor the contract on one nested card refuses whole, loudly, naming that card
/// (01-storage-format.md § Frontmatter: "every copy flow that rewrites descendants' `index.md`
/// preflights the entire subtree and refuses whole, loudly, naming the offending item never a
/// partial copy, never a silently unstamped descendant").
///
/// This **retired the former root-strict/nested-lenient split**, which copied an unreadable or
/// readable-but-uneditable nested `index.md` byte-verbatim and simply skipped its stamp. The
/// leniency read as kindness and was in fact the one verdict 01's doctrine forbids: "leniency is
/// recovering recoverable issues through reliable heuristics never accepting loss that could
/// surprise the user 'proceed partially, lose a little' is never a verdict". A silently
/// unstamped descendant carries a stale `modified-by` and, since 2026-07-29, a *live tracker
/// claim* into a second local object which is exactly the surprise. The finest-grain precedent
/// covers identity collisions (a per-folder repair that loses nothing), not skipped contract work.
///
/// The preflight runs over the **source**, so a refusal costs nothing on disk: nothing is copied,
/// nothing is renamed, nothing has to be cleaned up. A folder with **no `index.md`**
/// interrupted-create residue is not an offense: it is copied and reminted like any other and
/// simply has nothing to stamp, the same skip the loader applies to it (`.missingIndex`).
///
/// **All-or-nothing at the destination**, unlike a move: any failure once copying has begun
/// removes the partially copied tree best-effort and rethrows, because a half-copied item is
/// pure residue nothing was there before, so there is no true state for a reload to show.
/// pure residue nothing was there before, so there is no true state for a reload to show. After
/// a clean preflight the only thing left to fail is disk, and that is what this catch is for.
/// The source is never touched on any path.
public static func copyItem(
at sourceFolder: URL,
@@ -794,6 +847,9 @@ public enum BoardWriter: Sendable {
// failure from here on in this call including inside the materialized-but-not-yet-
// stamped tree below reuses the enriched value.
operation = try checkIndexIsRewritable(inItemFolder: sourceFolder, operation: operation)
// The transaction's preflight, over the *source*: a subtree that cannot honor the copy
// contract refuses here, where nothing has been materialized and there is nothing to undo.
try checkCopiedDescendantsAreStampable(of: sourceFolder, operation: operation)
let rank = try destinationOrder(order, inParent: destinationParent, operation: operation)
@@ -816,9 +872,7 @@ public enum BoardWriter: Sendable {
let now = Date()
try updateIndex(inItemFolder: root, operation: operation) { document in
if case .born = stamps {
document.set(FrontmatterKeys.created, to: .date(now))
}
applyCopyContract(to: &document, stamps: stamps, now: now)
document.set(FrontmatterKeys.order, to: .double(rank))
}
for folder in copied {
@@ -864,13 +918,102 @@ public enum BoardWriter: Sendable {
}
}
/// Stamps one copied folder below the root best-effort by design (see `copyItem`): a
/// missing, unreadable, or uneditable `index.md` is left exactly as the copy found it rather
/// than failing the gesture. `order` is not touched: a nested item keeps its rank among its
/// own siblings, which travelled with it.
/// **The copy contract's frontmatter edits**, applied to every folder an item-level copy
/// materializes root and descendants alike, in one place so the two can never disagree about
/// what a copy owes.
///
/// Two clauses, and `updateIndex` adds the third:
///
/// - **`created` per `stamps`** `.fork` keeps it (a copy really was created when its original
/// was), `.born` restamps it, because a board or card made from a template is born today
/// (09-templates.md).
/// - **The reserved tracker keys go** `remote` and `remote-state`, at every level
/// (01-storage-format.md § Fractal layout Rules, ruled 2026-07-29: "**Item-level copies sever
/// tracker identity** because two local objects must never both claim to be the same remote
/// object"). Content preserved, mapping severed. Nothing reads the keys until Teams, and that is
/// the argument rather than an objection: the copies made today are the boards Teams will meet,
/// so the sever costs nothing now and spares a stale double-claim later. `FrontmatterDocument.remove`
/// takes every occurrence, so a hand-duplicated key cannot leave a twin behind to resurrect the
/// claim.
/// - **`modified` stamped and `modified-by` cleared** come from `updateIndex`, because a copy is an
/// app write like any other (§ Frontmatter) not something this function has to remember.
///
/// `order` is deliberately absent: the copied *root* takes its new rank from its caller, and a
/// nested item keeps its rank among its own siblings, which travelled with it.
///
/// **Whole-board forks do not call this at all** Duplicate and Save as Template carry bytes
/// verbatim, GUIDs, timestamps and tracker keys included (01 Identity lifecycle's carve-out).
static func applyCopyContract(
to document: inout FrontmatterDocument,
stamps: CopyStamps,
now: Date
) {
if case .born = stamps {
document.set(FrontmatterKeys.created, to: .date(now))
}
document.remove(FrontmatterKeys.remote)
document.remove(FrontmatterKeys.remoteState)
}
/// **The copy transaction's preflight**: every identity-bearing folder beneath `folder` whose
/// `index.md` the copy contract will rewrite, checked for readability and editability *before*
/// anything is materialized and the first offender refuses the whole copy, named
/// (01-storage-format.md § Frontmatter, ruled 2026-07-29: "preflights the entire subtree and
/// refuses whole, loudly, naming the offending item").
///
/// **Naming the offender is the point**, so the thrown error is re-enriched with *that* item's
/// title rather than the copy root's: a refusal reading "Couldn't copy 'Sprint 12'" when the
/// unwritable file is one card inside it would send the user looking in the wrong place. A file
/// that cannot be read at all has no title to offer, and its path which the error always
/// carries is then the whole of what can honestly be said about it.
///
/// **A folder with no `index.md` is not an offense** and is skipped: it is interrupted-create
/// residue, the loader skips it too (`.missingIndex`), and there is no contract work to fail. The
/// walk is `identityDescendants`', which is `remintDescendants`' own reach so the set checked
/// here is exactly the set that will be stamped, never a superset that could refuse a copy over a
/// file nobody was going to touch.
///
/// **Internal rather than `private`**: template instantiation preflights its own tree with this,
/// for `remintDescendants`' reason one definition of what a copy owes its descendants.
static func checkCopiedDescendantsAreStampable(
of folder: URL,
operation: WriteOperation
) throws(BoardWriteError) {
for descendant in identityDescendants(of: folder) {
let indexURL = descendant.appendingPathComponent(BoardLoader.indexFileName)
guard FileManager.default.fileExists(atPath: indexURL.path) else { continue }
let document = try readDocument(at: indexURL, operation: operation)
try checkEditable(document, at: indexURL, operation: operation.withTitle(document.title.value))
}
}
/// Every identity-bearing folder beneath `folder`, depth first `remintDescendants`' walk with
/// the renaming taken out, so the preflight and the remint can never disagree about which folders
/// a copy materializes as items.
private static func identityDescendants(of folder: URL) -> [URL] {
var found: [URL] = []
for child in childCandidates(of: folder) {
found.append(child)
found.append(contentsOf: identityDescendants(of: child))
}
return found
}
/// Stamps one copied folder below the root **strictly**, since the preflight has already cleared
/// the whole subtree (`checkCopiedDescendantsAreStampable`): an `index.md` that cannot be read or
/// edited here is a disk failure between the two reads, not a shape to tolerate, and it fails the
/// copy like any other mid-flight failure (whose partial result the caller removes wholesale).
///
/// The former best-effort posture copy it verbatim, skip its stamp is retired with the
/// root-strict/nested-lenient split (see `copyItem`): a silently unstamped descendant is a
/// descendant still carrying somebody else's `modified-by` and, since 2026-07-29, somebody else's
/// tracker claim.
///
/// A folder with **no `index.md`** is still skipped, and for a different reason entirely: there is
/// nothing there to stamp (interrupted-create residue, which the loader skips too).
///
/// **Internal rather than `private`**, with `remintDescendants` and for its reason: an
/// instantiated board's lanes and cards are stamped by this exact rule, leniency included.
/// instantiated board's lanes and cards are stamped by this exact rule.
static func stampCopiedDescendant(
at folder: URL,
stamps: CopyStamps,
@@ -878,85 +1021,13 @@ public enum BoardWriter: Sendable {
operation: WriteOperation
) throws(BoardWriteError) {
let indexURL = folder.appendingPathComponent(BoardLoader.indexFileName)
guard FileManager.default.fileExists(atPath: indexURL.path),
let copied = try? readDocument(at: indexURL, operation: operation),
copied.uneditableShape == nil
else { return }
guard FileManager.default.fileExists(atPath: indexURL.path) else { return }
try updateIndex(inItemFolder: folder, operation: operation) { document in
if case .born = stamps {
document.set(FrontmatterKeys.created, to: .date(now))
}
applyCopyContract(to: &document, stamps: stamps, now: now)
}
}
/// Materializes an item from **supplied `index.md` text** rather than from a folder on disk
/// the clipboard's staging-less fallback (04-interactions.md Clipboard: "if the staged
/// snapshot is missing or unreadable at paste time, paste falls back to the embedded
/// `index.md` content intact, attachments absent").
///
/// **The text is written byte-faithfully, because it *is* the source bytes.** It was captured
/// verbatim at copy time and travels through the manifest untouched, so this call writes it as
/// given unknown keys, comments, blank lines, line endings, body and all rather than
/// re-serializing anything. That is the round-trip guarantee applied to a file the app is
/// minting from bytes it was handed (01-storage-format.md § Fractal layout Rules).
///
/// **Fresh identity, fork stamps** the same semantics `copyItem` gives an ordinary copy, and
/// necessarily so: this is a copy that happened to arrive as text. Every folder is a fresh mint,
/// `created` survives in the supplied bytes (a duplicate is a fork), and the root's `order` and
/// `modified` are rewritten by the closing `updateIndex`, which also clears `modified-by`.
///
/// `children` are a **lane's** cards, each its own supplied `index.md`, materialized under the
/// new root in the order given and deliberately *not* rewritten: "a nested item keeps its rank
/// among its own siblings, which travelled with it" (`copyItem`'s rule). A card passes none.
///
/// **The root gets `copyItem`'s strictness and the children get its leniency.** The root must be
/// rewritten it needs its new `order` so unparseable or uneditable text fails the call;
/// a child is never rewritten, so whatever it is arrives exactly as it was.
///
/// **All-or-nothing at the destination**, `copyItem`'s rule for its reason: any failure once the
/// folder exists removes the partial tree best-effort and rethrows, because a half-materialized
/// item is pure residue nothing was there before.
public static func materializeItem(
inParent destinationParent: URL,
indexText: String,
children: [String] = [],
order: Double?
) throws(BoardWriteError) -> ItemID {
let operation = WriteOperation.copy(title: nil)
try checkIsDirectory(destinationParent, describedAs: "destination parent folder", operation: operation)
let rank = try destinationOrder(order, inParent: destinationParent, operation: operation)
// The same mint the create path uses, so every identity this app materializes is materialized
// one way fresh lowercase UUIDv4, folder and all.
let root = try mintUUIDFolder(in: destinationParent, operation: operation)
do throws(BoardWriteError) {
try atomicReplace(
text: indexText,
at: root.appendingPathComponent(BoardLoader.indexFileName),
operation: operation
)
for child in children {
let childFolder = try mintUUIDFolder(in: root, operation: operation)
try atomicReplace(
text: child,
at: childFolder.appendingPathComponent(BoardLoader.indexFileName),
operation: operation
)
}
try updateIndex(inItemFolder: root, operation: operation) { document in
document.set(FrontmatterKeys.order, to: .double(rank))
}
} catch {
try? FileManager.default.removeItem(at: root)
throw error
}
return ItemID(rawValue: root.lastPathComponent)
}
// MARK: - The materialized trash
/// `<boardRoot>/.trash/` the board's trash container, named but not created.
@@ -994,10 +1065,13 @@ public enum BoardWriter: Sendable {
/// *snapshot*, which the store holds and this stateless layer does not. Value-passing keeps
/// the seam: the Writer takes a rank, the store computes it.
///
/// **The `modified` stamp is the point, not a side effect.** Deletion is the one exception to
/// moves-don't-stamp "deletion is an edit to the card's story" and the stamp is what a
/// future age-based auto-purge reads. It falls out of `updateIndex` here rather than being
/// asked for, which is why there is nothing extra in step 5.
/// **The `modified` stamp is the point, not a side effect** and it needs no exception to earn
/// it. The trash move changes the card's *container*, which is the whole predicate
/// (`WriteOperation.rewritesOrderOnly`, refined 2026-07-30): "deletion is an edit to the item's
/// story", so it stamps exactly as a cross-lane move does, and the stamp is what a future
/// age-based auto-purge reads. It falls out of `updateIndex` here rather than being asked for,
/// which is why there is nothing extra in step 5 and why the reorders-don't-stamp rule needs no
/// trash carve-out to coexist with this call.
///
/// **Collision inside `.trash/` is impossible by construction**, and it is checked anyway. The
/// card is a resident of this very board, and board-wide uniqueness now spans lanes *and* the
@@ -1358,8 +1432,8 @@ public enum BoardWriter: Sendable {
/// trees.
/// - **The bytes are written verbatim** nothing is stamped, nothing is re-serialized, no
/// `index.md` is parsed. This replays; it does not edit.
/// - **All-or-nothing**: any failure removes the partial tree best-effort and rethrows, the
/// `materializeItem` rule a half-restored lane is pure residue, since nothing was there.
/// - **All-or-nothing**: any failure removes the partial tree best-effort and rethrows,
/// `copyItem`'s rule a half-restored lane is pure residue, since nothing was there.
///
/// `folder`'s own name governs, not `snapshot.name`: a caller restoring to the path it removed
/// passes the same URL, and the snapshot's name is carried for identification, not as an
@@ -1533,8 +1607,8 @@ public enum BoardWriter: Sendable {
/// Puts a removed item's folder back, at its own path and with its own bytes the redo half of
/// an undone create (13-native-undo.md Rules).
///
/// **The identity is the point.** `createLane`/`createCard` mint a fresh UUID and
/// `materializeItem` mints a fresh one too, so neither can replay a create: redoing through them
/// **The identity is the point.** `createLane`/`createCard` mint a fresh UUID, so they cannot
/// replay a create: redoing through them
/// would produce a *different* item, and every step registered above this one on the stack
/// (a rename, a move, a body edit of that very card) would then name nothing. This call takes the
/// path as given.
@@ -1577,7 +1651,7 @@ public enum BoardWriter: Sendable {
operation: operation
)
} catch {
// `materializeItem`'s all-or-nothing rule: a half-made folder is pure residue, since
// `copyItem`'s all-or-nothing rule: a half-made folder is pure residue, since
// nothing was there before.
try? FileManager.default.removeItem(at: itemFolder)
throw error
@@ -2178,6 +2252,14 @@ public enum BoardWriter: Sendable {
/// **It stamps nothing.** A heal that only renames never opens an `index.md`, so the existing
/// write discipline decides and there is no rule to add (§ Validation and healing).
///
/// **Level-uniform** (extended 2026-07-29): the squatter's own `location` names the folder the
/// claimed name lives in the board root, or a card's folder for a file wearing `attachments`. That
/// is the whole of the difference, which is the point of the ruling: one ladder, one notice, one
/// write, whichever level the name is claimed at. A card whose folder has since gone takes the
/// re-verification's `nil` path like any other vanished defect.
///
/// - Parameter root: the board root. The squatter's location is resolved against it, so a board
/// renamed since the load heals at its new location.
/// - Returns: the name the squatter now has, or `nil` when the defect was already gone.
@discardableResult
public static func displaceClaimedName(
@@ -2185,13 +2267,14 @@ public enum BoardWriter: Sendable {
atBoardRoot root: URL
) throws(BoardWriteError) -> String? {
let operation = WriteOperation.displaceClaimedName(name: squatter.name)
let occupied = root.appendingPathComponent(squatter.name)
let container = squatter.location.folder(under: root)
let occupied = container.appendingPathComponent(squatter.name)
guard let found = IntegrityRules.node(at: occupied), found != squatter.expected else {
return nil
}
let freed = freshName(for: squatter.name, in: root)
let destination = root.appendingPathComponent(freed)
let freed = freshName(for: squatter.name, in: container)
let destination = container.appendingPathComponent(freed)
do {
try FileManager.default.moveItem(at: occupied, to: destination)
} catch {
@@ -2565,6 +2648,23 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case move(title: String?)
case reorder(title: String?)
case copy(title: String?)
/// V **a paste that refused before it wrote anything** (04-interactions.md Clipboard,
/// re-ruled 2026-07-29: "A paste whose staged snapshot is missing or unreadable refuses loudly
/// never degrades the paste produces **nothing**, and a one-shot failure banner names it from
/// the manifest's metadata").
///
/// **Its own case rather than a fold into `.copy`**, on `.rename`'s and `.duplicateBoard`'s
/// reasoning: the user pressed *Paste*, and a banner telling them the app "couldn't copy 'Fix
/// login'" would name a gesture they never made. It is also the one operation in this vocabulary
/// the Writer itself never performs a paste's *arrivals* are `.copy` and `.move` because the
/// refusal happens at the clipboard's preflight, before any arrival is materialized; the
/// vocabulary grows with the surfaces, and the surface here is the refusal.
///
/// `title` is the offending entry's, read off the manifest's own metadata (which is exactly what
/// the embedded `index.md` is kept for now that it is never a materialization source), `nil` for
/// an untitled item.
case paste(title: String?)
/// / a card moving into `.trash/` (`deleteCardToTrash`) or a lane being removed
/// outright (`removeLane`). The word the user pressed, whichever staging it took.
///
@@ -2733,6 +2833,10 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case .move: .move(title: title)
case .reorder: .reorder(title: title)
case .copy: .copy(title: title)
// Identity, like `.repairDuplicateID` and for its reason: a refused paste never opens an
// `index.md`, so there is no `readDocument` to enrich from its title arrives already filled
// in from the manifest entry the refusal names.
case .paste: self
case .delete: .delete(title: title)
case .purge: .purge(title: title)
case .migrateTombstone: .migrateTombstone(title: title)
@@ -2747,6 +2851,46 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
}
}
/// **Whether this rewrite only restates the item's position among its container's members**
/// the reorders-don't-stamp predicate (01-storage-format.md § Frontmatter `modified`'s scope,
/// ruled 2026-07-29 as moves-don't-stamp, refined 2026-07-30).
///
/// One question decides it: **does the rewrite change the item's container?** If it does a
/// cross-lane move, a cross-board arrival, a move into or out of `.trash/` the item's story
/// changed (which lane a card lives in is state) and the write stamps `modified` and clears
/// `modified-by` like any content write. If it does not a card reordered among its lane's
/// siblings, a lane reordered on the board, a renumber's whole-lane rescale only `order` is
/// rewritten: no stamp, no clear. Where an item *stands in line* is presentation, and `order` is
/// logically the container's property that the format happens to store inside the member's file.
///
/// **The vocabulary already draws the line**, which is why this is a property here rather than a
/// flag threaded through `updateIndex`'s call sites:
///
/// - `.reorder` is the same-container case by construction `moveItem` decides it from the source
/// parent and the destination parent before it touches disk (`isSameLocation`), and the two
/// inverse paths that rewrite a rank directly (`BoardStore.setOrder`, the within-lane sort) are
/// same-container for the same reason: a lane's parent is the board root, and a card the sort
/// permutes never leaves its lane.
/// - `.renumberChildren` is the midpoint-exhaustion rescale "order-only rewrites, so no
/// `modified` stamp and no `modified-by` clear" (01 § Ordering, verbatim).
/// - **Everything else stamps.** `.move` covers every container change including the trash's, and
/// there is deliberately **no trash case anywhere**: the trash move stamps because every
/// container change stamps, so a branch for it would be a second rule saying the same thing.
///
/// Exhaustive with no `default`, like every other switch over this enum: a new operation has to
/// answer "does this rewrite content?" before it compiles.
public var rewritesOrderOnly: Bool {
switch self {
case .reorder, .renumberChildren:
true
case .createBoard, .createLane, .createCard, .move, .copy, .delete, .purge, .migrateTombstone,
.style, .resize, .rename, .duplicateBoard, .saveAsTemplate, .paste, .importAttachment,
.listAttachments, .removeAttachment, .relocateLooseFile, .agentGuide, .displaceClaimedName,
.repairDuplicateID, .toggleTask, .editBody, .rawSource:
false
}
}
/// A short imperative phrase `"move 'Fix login'"`, `"create card"`, `"import attachment
/// 'photo.png'"` for logs and diagnostics **only**: `BoardWriteError.description` (test
/// failures, `po error`, console output), never the banner's text. The banner owns every
@@ -2761,6 +2905,7 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case let .move(title): Self.phrase("move", title)
case let .reorder(title): Self.phrase("reorder", title)
case let .copy(title): Self.phrase("copy", title)
case let .paste(title): Self.phrase("paste", title)
case let .delete(title): Self.phrase("delete", title)
case let .purge(title): Self.phrase("purge", title)
case let .migrateTombstone(title): Self.phrase("migrate the legacy 'deleted' key on", title)
@@ -2844,6 +2989,18 @@ public struct BoardWriteError: Error, Sendable, Equatable, CustomStringConvertib
/// the *proposal* is not, so nothing was attempted and nothing changed.
case invalidSource(BoardLoadError)
/// **The bytes a paste was to reproduce are not there** the staged clipboard snapshot is
/// missing or unreadable, so the paste produces nothing rather than a hollowed item
/// (04-interactions.md Clipboard, re-ruled 2026-07-29 Finder's invariant, and 01's
/// leniency doctrine: "proceed partially, lose a little" is never a verdict).
///
/// **Payload-free on purpose.** There is exactly one thing to say about it, the banner owns
/// the words (`BannerCenter.causePhrase`), and the offending item is already named by the
/// operation's own title so a free-form message here could only be a second, worse copy of
/// a sentence that lives one layer up. That also keeps it distinct from `.unreadable`, whose
/// message is a developer's diagnostic about a file the app *did* open.
case clipboardContentGone
public var description: String {
switch self {
case let .unreadable(message):
@@ -2856,6 +3013,8 @@ public struct BoardWriteError: Error, Sendable, Equatable, CustomStringConvertib
message
case let .invalidSource(error):
"the source text wouldn't load: \(error.description)"
case .clipboardContentGone:
"the copied content is no longer staged"
}
}
}