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.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user