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:
@@ -181,8 +181,8 @@ struct ClipboardManifestTests {
|
||||
#expect(ClipboardManifest(data: data) == nil)
|
||||
}
|
||||
|
||||
@Test("A lane entry's lost-attachment count totals its cards'")
|
||||
func lostAttachments() {
|
||||
@Test("A lane entry's attachment count totals its cards'")
|
||||
func totalAttachments() {
|
||||
let lane = ClipboardManifest.Entry(
|
||||
id: Ident.lane1,
|
||||
folder: Ident.lane1,
|
||||
@@ -194,7 +194,7 @@ struct ClipboardManifestTests {
|
||||
.init(id: Ident.card2, title: "Second", index: "b", attachmentCount: 1),
|
||||
]
|
||||
)
|
||||
#expect(lane.lostAttachmentCount == 3)
|
||||
#expect(lane.totalAttachmentCount == 3)
|
||||
}
|
||||
|
||||
@Test("The plain-text rendering is the titles, untitled items rendered as the board renders them")
|
||||
@@ -285,7 +285,7 @@ struct ClipboardCopyTests {
|
||||
#expect(manifest.kind == .lane)
|
||||
#expect(manifest.entries.map(\.id) == [Ident.lane1])
|
||||
#expect(manifest.entries[0].cards.map(\.id) == [Ident.card1, Ident.card2])
|
||||
#expect(manifest.entries[0].lostAttachmentCount == 2)
|
||||
#expect(manifest.entries[0].totalAttachmentCount == 2)
|
||||
}
|
||||
|
||||
@Test("A trash selection copies out, container recorded")
|
||||
@@ -830,73 +830,86 @@ struct PasteTargetTests {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The degraded paste's phrasing
|
||||
// MARK: - The refused paste's phrasing
|
||||
|
||||
@Suite("BannerCenter ▸ degraded paste")
|
||||
struct DegradedPasteBannerTests {
|
||||
/// **04-interactions.md ▸ Clipboard, re-ruled 2026-07-29** — refuse, never degrade:
|
||||
///
|
||||
/// > 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 ("Couldn't paste 'Fix login' — the copied content is gone").
|
||||
///
|
||||
/// The retired suite these replace pinned `degradedPasteMessage(for:)` and its loss row — "Pasted
|
||||
/// 'Fix login' without its 3 attachments". Both are gone with the degraded materialization: nothing
|
||||
/// arrives, so there is no partial arrival to account for.
|
||||
@Suite("BannerCenter ▸ refused paste")
|
||||
struct RefusedPasteBannerTests {
|
||||
|
||||
/// 04's own example sentence, composed the way every failure headline is: the action clause the
|
||||
/// banner owns, an em dash, the cause.
|
||||
@Test("04's own example sentence")
|
||||
@MainActor
|
||||
func theExampleSentence() {
|
||||
#expect(BannerCenter.degradedPasteMessage(
|
||||
for: [.init(title: "Fix login", attachments: 3)]
|
||||
) == "Pasted 'Fix login' without its 3 attachments")
|
||||
let center = BannerCenter()
|
||||
center.postRefusedPaste(title: "Fix login", stagedAt: "/tmp/staging/abc")
|
||||
|
||||
#expect(center.oneShots.count == 1)
|
||||
let headline = try? #require(center.oneShots.first).error
|
||||
#expect(headline.map(BannerCenter.headline(for:)) == "Couldn't paste 'Fix login' — the copied content is gone")
|
||||
}
|
||||
|
||||
@Test("One attachment is singular")
|
||||
func singular() {
|
||||
#expect(BannerCenter.degradedPasteMessage(
|
||||
for: [.init(title: "Fix login", attachments: 1)]
|
||||
) == "Pasted 'Fix login' without its attachment")
|
||||
}
|
||||
|
||||
@Test("An untitled item is 'the item', never the Untitled rendering")
|
||||
/// "Untitled" is a rendering, never a value (03-board-ui.md § Card face), so an untitled entry is
|
||||
/// "the item" — `actionPhrase`'s standing convention for a failure with no title to quote.
|
||||
@Test("An untitled entry is 'the item', never the Untitled rendering")
|
||||
@MainActor
|
||||
func untitled() {
|
||||
#expect(BannerCenter.degradedPasteMessage(
|
||||
for: [.init(title: nil, attachments: 2)]
|
||||
) == "Pasted the item without its 2 attachments")
|
||||
}
|
||||
|
||||
@Test("Several items total their attachments rather than listing titles")
|
||||
func several() {
|
||||
#expect(BannerCenter.degradedPasteMessage(
|
||||
for: [.init(title: "A", attachments: 2), .init(title: "B", attachments: 3)]
|
||||
) == "Pasted 2 items without their 5 attachments")
|
||||
}
|
||||
|
||||
@Test("Nothing lost says nothing")
|
||||
func nothingLost() {
|
||||
#expect(BannerCenter.degradedPasteMessage(for: []) == nil)
|
||||
#expect(BannerCenter.degradedPasteMessage(for: [.init(title: "A", attachments: 0)]) == nil)
|
||||
}
|
||||
|
||||
@Test("Posting an empty loss list adds no row")
|
||||
@MainActor
|
||||
func postingNothing() {
|
||||
let center = BannerCenter()
|
||||
center.postDegradedPaste([])
|
||||
#expect(center.losses.isEmpty)
|
||||
center.postRefusedPaste(title: nil, stagedAt: "/tmp/staging/abc")
|
||||
let error = try? #require(center.oneShots.first).error
|
||||
#expect(error.map(BannerCenter.headline(for:)) == "Couldn't paste the item — the copied content is gone")
|
||||
}
|
||||
|
||||
/// **The pivot, stated as a class change**: the degraded paste was a loss row because the items
|
||||
/// landed and only their attachments did not. A refusal is *a write that did not happen*, which is
|
||||
/// 02-architecture.md's own definition of a one-shot — so it ranks with the true failures, carries
|
||||
/// the error tone, and posts no loss row at all.
|
||||
@Test("A refused paste is an error-tone one-shot, not a loss row")
|
||||
@MainActor
|
||||
func refusalIsAOneShotNotALossRow() {
|
||||
let center = BannerCenter()
|
||||
center.postRefusedPaste(title: "Fix login", stagedAt: "/tmp/staging/abc")
|
||||
|
||||
#expect(center.losses.isEmpty, "the degraded paste's loss row is retired")
|
||||
#expect(center.signposts.isEmpty)
|
||||
}
|
||||
|
||||
@Test("A degraded paste lands in the loss class, not the signpost class")
|
||||
@MainActor
|
||||
func postingLandsAsALossRow() {
|
||||
// Settled 2026-07-28 (DESIGN/02-architecture.md § The banner surface, "Loss rows"): the
|
||||
// degraded paste retoned from a signpost onto the new warning-tone loss class — content
|
||||
// that didn't arrive though nothing failed, ranking below the true failures and above the
|
||||
// ambient notices rather than at the bottom of the strip.
|
||||
let center = BannerCenter()
|
||||
center.postDegradedPaste([.init(title: "Fix login", attachments: 3)])
|
||||
|
||||
#expect(center.losses.count == 1)
|
||||
#expect(center.losses.first?.message == "Pasted 'Fix login' without its 3 attachments")
|
||||
#expect(center.signposts.isEmpty, "the degraded paste no longer posts a signpost")
|
||||
|
||||
let rows = BannerCenter.rows(
|
||||
lock: nil, breakage: nil, oneShots: [], losses: center.losses, suspension: nil, operations: []
|
||||
lock: nil, breakage: nil, oneShots: center.oneShots, losses: [], suspension: nil, operations: []
|
||||
)
|
||||
#expect(rows.count == 1)
|
||||
#expect(rows[0].tone == .warning)
|
||||
#expect(rows[0].dismissID == center.losses.first?.id)
|
||||
#expect(rows[0].tone == .error)
|
||||
#expect(rows[0].dismissID == center.oneShots.first?.id)
|
||||
}
|
||||
|
||||
/// The staging path is what the error names, so a bug report about a refusal has something to go
|
||||
/// on — the file that was not there.
|
||||
@Test("The refusal names the staged path it could not find")
|
||||
@MainActor
|
||||
func namesTheStagedPath() {
|
||||
let center = BannerCenter()
|
||||
center.postRefusedPaste(title: "Fix login", stagedAt: "/tmp/staging/abc")
|
||||
#expect(center.oneShots.first?.error.path == "/tmp/staging/abc")
|
||||
#expect(center.oneShots.first?.error.reason == .clipboardContentGone)
|
||||
#expect(center.oneShots.first?.error.operation == .paste(title: "Fix login"))
|
||||
}
|
||||
|
||||
/// **The loss class survives the retirement** — 02's warning-tone class still has live producers
|
||||
/// (a Finder drop that skipped folders, the app's own relocation and repair notices); only the
|
||||
/// degraded-paste row left it.
|
||||
@Test("The loss class still has its other producers")
|
||||
@MainActor
|
||||
func theLossClassSurvives() {
|
||||
let center = BannerCenter()
|
||||
center.postSkippedFolders(count: 2)
|
||||
#expect(center.losses.count == 1)
|
||||
#expect(center.losses.first?.message == "Folders can't be attached — 2 skipped")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user