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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user