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
+52 -71
View File
@@ -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.
///