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
+61 -46
View File
@@ -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