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
+38 -34
View File
@@ -14,8 +14,10 @@ import os
/// folder trees, attachments and strays and all is **staged** under
/// `<group container>/Library/Application Support/Clipboard/<copyID>/`, so a paste reproduces the item
/// byte-for-byte across boards rather than reconstructing it from a summary. The manifest's embedded
/// `index.md` per entry is the fallback when a snapshot is missing, and a fallback paste is **loud**:
/// a banner names exactly what was lost.
/// `index.md` per entry is **identification metadata only** menu validation, the refusal's wording,
/// the plain-text flavor and never a materialization source: a paste whose staged snapshot is
/// missing or unreadable **refuses whole and writes nothing** (04-interactions.md Clipboard,
/// re-ruled 2026-07-29 Finder's invariant: an item arrives whole or not at all).
///
/// **The store is shared by every installed edition** (12-editions.md Both editions installed, ruled
/// 2026-07-29): the group container is one container, so C in base pastes full-fidelity in Pro. The
@@ -116,8 +118,9 @@ public final class ClipboardStore {
/// > edition pastes **full-fidelity** in the other snapshot, attachments and all.
///
/// Nothing about the lifecycle changes: both editions read the same pasteboard, so both sweeps
/// compute the same answer from the same input, and the degraded embedded-`index.md` fallback stays
/// for genuinely missing snapshots rather than being the structural cross-edition outcome.
/// compute the same answer from the same input. The shared home is also what keeps the refusal a
/// rare corner rather than the structural cross-edition outcome a copy in one edition pastes
/// full-fidelity in the other, so neither has to reach for bytes that are not there.
public static var defaultStagingRoot: URL {
AppGroup.stateDirectory.appendingPathComponent("Clipboard", isDirectory: true)
}
@@ -182,9 +185,10 @@ public final class ClipboardStore {
/// The order is the contract: capture from the snapshot (main actor, no I/O every item's
/// `index.md` is already parsed into the snapshot and `FrontmatterDocument.serialized()` returns
/// it verbatim), schedule the snapshots behind it, then write the pasteboard, then sweep. The
/// pasteboard is written *before* the copies land, which is safe precisely because the manifest
/// carries the fallback text: a paste that somehow beat the chain would still materialize the
/// right items.
/// pasteboard is written *before* the copies land, which is safe because a paste **awaits the same
/// chain** (`paste(into:)`): it can never read a half-written snapshot, so it never sees a tree the
/// staging has not finished. This used to lean on the manifest's fallback text instead; with
/// refuse-don't-degrade the chain is the whole guarantee, and it is the stronger one.
private func write(from store: BoardStore, cut: Bool) {
guard let capture = Self.capture(selection: store.selection, snapshot: store.snapshot) else { return }
@@ -348,9 +352,8 @@ public final class ClipboardStore {
// doing nothing.
guard payload?.copyID == manifest.copyID else { return }
store.transient.noteUserCreation()
if let move = armedMove(for: manifest) {
store.transient.noteUserCreation()
let sources = move.folders.map(BoardStore.ItemSource.folder)
switch plan {
case let .cards(target):
@@ -373,31 +376,29 @@ public final class ClipboardStore {
return
}
// The copy path the staged snapshot per entry, or the embedded `index.md` where that
// snapshot is missing or unreadable. Mixed is legal and is the honest outcome of a partial
// staging failure: the entries that have snapshots arrive whole.
// **The copy path's preflight: refuse, never degrade** (04-interactions.md Clipboard,
// re-ruled 2026-07-29). Every entry must have its staged snapshot on disk *before* anything is
// materialized the first one that does not refuses the whole paste, names itself from the
// manifest's metadata, and writes nothing at all. All-or-nothing for the whole paste, which is
// the copies-are-transactions posture (01-storage-format.md § Frontmatter) read one level up:
// the transaction is the gesture, not the entry.
let stagingDir = stagingRoot.appendingPathComponent(manifest.copyID, isDirectory: true)
var sources: [BoardStore.ItemSource] = []
var losses: [BannerCenter.AttachmentLoss] = []
for entry in manifest.entries {
let staged = stagingDir.appendingPathComponent(entry.folder, isDirectory: true)
if FileManager.default.fileExists(
guard FileManager.default.fileExists(
atPath: staged.appendingPathComponent(BoardLoader.indexFileName).path
) {
sources.append(.folder(staged))
continue
}
sources.append(.text(index: entry.index, cards: entry.cards.map(\.index)))
// "A degraded paste is loud, never silent a one-shot banner names exactly what was
// lost." An entry with no attachments lost nothing its content is intact and its bytes
// are the source bytes so it contributes no row.
if entry.lostAttachmentCount > 0 {
losses.append(BannerCenter.AttachmentLoss(
title: entry.title,
attachments: entry.lostAttachmentCount
))
) else {
// The offending entry, named and the destination's search is left exactly as it was.
// "Any user-initiated creation on the board clears the query" (04 Search) is a rule
// about creations, and this paste created nothing; the preflight therefore runs *before*
// `noteUserCreation`, so a refusal costs the user neither content nor their filter.
store.banners.postRefusedPaste(title: entry.title, stagedAt: staged.path)
return
}
sources.append(.folder(staged))
}
store.transient.noteUserCreation()
// A card copied out of the trash needs nothing done to it on arrival: it carries no
// `deleted:` key, because there is no such key any more (03-board-ui.md § Trash, resettled
@@ -419,7 +420,6 @@ public final class ClipboardStore {
normalizingLooseFiles: true
)
}
store.banners.postDegradedPaste(losses)
}
/// The armed cut's surviving originals, in flatten order and as folders under the **source**
@@ -486,9 +486,11 @@ public final class ClipboardStore {
let destination: URL
}
/// Appends this copy's snapshots to the staging chain. Best-effort per item: one that fails to
/// copy simply falls back to the manifest's embedded `index.md` at paste time, which is the
/// degraded paste the banner already has words for.
/// Appends this copy's snapshots to the staging chain. Best-effort per item, and the *consequence*
/// of a failure changed with the refuse-don't-degrade ruling: an item whose snapshot never landed
/// makes the next paste **refuse whole**, naming it (`perform`'s preflight), rather than
/// materializing it hollow from the manifest's embedded `index.md`. Failing to stage is therefore
/// as loud as it should be, one gesture later.
private func stage(_ jobs: [StagingJob], into stagingDir: URL) {
enqueue { [jobs, stagingDir] in
guard (try? FileManager.default.createDirectory(
@@ -628,9 +630,11 @@ public final class ClipboardStore {
/// the app already states once.
///
/// **The index text comes from the snapshot, not from disk.** `FrontmatterDocument` edits by line
/// span, so `serialized()` on an untouched document returns the file's bytes exactly which
/// makes the manifest's fallback text genuinely *the source bytes* while costing C no file I/O
/// at all, even for a lane carrying two hundred cards.
/// span, so `serialized()` on an untouched document returns the file's bytes exactly which makes
/// the manifest's embedded text a faithful record of the item while costing C no file I/O at all,
/// even for a lane carrying two hundred cards. It is **identification metadata**, not a
/// materialization source (see the type comment): the refusal's wording and the plain-text flavor
/// read it, and nothing writes it.
static func capture(
selection: ItemReferenceSet,
snapshot: BoardModel