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:
@@ -21,9 +21,11 @@ import UniformTypeIdentifiers
|
||||
/// the full folder snapshots a paste reproduces byte-for-byte from — and to a pending cut. It is
|
||||
/// also the whole of "the snapshot survives relaunch exactly as long as the pasteboard still points
|
||||
/// at it": a sweep keeps the one directory this id names and collects every other.
|
||||
/// - Each `Entry` embeds the item's complete `index.md` text, so a paste still lands when the
|
||||
/// snapshot is missing or unreadable — "the staging-less fallback: content intact, attachments
|
||||
/// absent", announced by a banner rather than discovered later.
|
||||
/// - Each `Entry` embeds the item's complete `index.md` text as **identification metadata**
|
||||
/// (04-interactions.md ▸ Clipboard, re-ruled 2026-07-29): menu validation, the refusal's wording,
|
||||
/// and the plain-text flavor read it. It is emphatically **not** a materialization source — a paste
|
||||
/// whose staged snapshot is missing or unreadable refuses whole and writes nothing, because "an item
|
||||
/// arrives whole — index, attachments, loose files — or not at all".
|
||||
///
|
||||
/// `kind` and `container` are the selection's own vocabulary (`SelectionKind`, `ItemContainer`) rather than
|
||||
/// near-copies of it: a clipboard payload is a selection that was copied, and the cards-XOR-lanes and
|
||||
@@ -67,15 +69,23 @@ public struct ClipboardManifest: Codable, Sendable, Equatable {
|
||||
public var folder: String
|
||||
|
||||
/// The title as written, or `nil` for an untitled item — "Untitled" is a rendering, never a
|
||||
/// value (03-board-ui.md § Card face). Feeds the plain-text representation and the degraded
|
||||
/// paste's banner.
|
||||
/// value (03-board-ui.md § Card face). Feeds the plain-text representation and the refused
|
||||
/// paste's banner, which names the offending entry from exactly this.
|
||||
public var title: String?
|
||||
|
||||
/// The complete `index.md` at copy time — the staging-less fallback's source bytes.
|
||||
/// The complete `index.md` at copy time — **identification metadata, never materialized**
|
||||
/// (see the type comment). Kept because it is what lets the app answer "what was on the
|
||||
/// clipboard" without touching the staging store: the plain-text flavor and a refusal's wording
|
||||
/// both come from here, and both have to work when the snapshot is exactly what is missing.
|
||||
public var index: String
|
||||
|
||||
/// How many files the item's own `attachments/` held. Zero for a lane, which has none; a
|
||||
/// lane's attachments are its cards' and are counted there.
|
||||
///
|
||||
/// Identification metadata like the rest of the entry. It used to feed the degraded paste's
|
||||
/// loss accounting ("Pasted 'Fix login' without its 3 attachments"), which is retired with the
|
||||
/// degraded paste itself — an item now arrives whole or not at all, so there is no partial
|
||||
/// arrival left to count.
|
||||
public var attachmentCount: Int
|
||||
|
||||
/// A **lane** entry's cards, index text and all — "a lane entry embeds its cards' too,
|
||||
@@ -83,9 +93,9 @@ public struct ClipboardManifest: Codable, Sendable, Equatable {
|
||||
///
|
||||
/// **Exactly the lane's cards**, which needs no filter: "a lane carries exactly its cards —
|
||||
/// the trash is board-level, so there is nothing lane-nested to strip or carry"
|
||||
/// (04-interactions.md ▸ Drag and drop, resettled 2026-07-28), and the fallback only ever
|
||||
/// materializes a copy — a cut's move carries the real folder whole and never comes near this
|
||||
/// array. So the embedded set is exactly what a fallback paste should produce.
|
||||
/// (04-interactions.md ▸ Drag and drop, resettled 2026-07-28). Like the lane's own `index`, the
|
||||
/// cards' text is identification metadata: it describes what the copy held, and nothing
|
||||
/// materializes from it.
|
||||
public var cards: [Card]
|
||||
|
||||
/// One card inside a copied lane.
|
||||
@@ -103,9 +113,9 @@ public struct ClipboardManifest: Codable, Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything a fallback paste of this entry would leave behind — its own attachments plus,
|
||||
/// for a lane, its cards'.
|
||||
public var lostAttachmentCount: Int {
|
||||
/// Every file this entry's subtree carried in an `attachments/` — its own plus, for a lane, its
|
||||
/// cards'. Identification metadata; nothing gates on it since the degraded paste retired.
|
||||
public var totalAttachmentCount: Int {
|
||||
attachmentCount + cards.reduce(0) { $0 + $1.attachmentCount }
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,8 @@ import os
|
||||
///
|
||||
/// A template is a board folder, so discovery is `BoardLoader.load` and instantiation is a tree copy
|
||||
/// plus the Writer's own remint-and-restamp machinery (`BoardWriter.remintDescendants`,
|
||||
/// `stampCopiedDescendant`, `updateIndex`). There is no template schema, no template catalog in
|
||||
/// `checkCopiedDescendantsAreStampable`, `applyCopyContract`, `stampCopiedDescendant`,
|
||||
/// `updateIndex`). There is no template schema, no template catalog in
|
||||
/// Swift, and no second copy path — which is 09's "dogfood" clause and 02-architecture.md's single
|
||||
/// write door, both held by having nothing here to hold them with.
|
||||
///
|
||||
@@ -379,19 +380,35 @@ enum TemplateEngine {
|
||||
/// The born half, on the tree already at the destination: fresh identities, fresh stamps, the
|
||||
/// chosen title, and the loose-file normalization an import boundary owes.
|
||||
///
|
||||
/// **The root is strict and the descendants are lenient**, which is `BoardWriter.copyItem`'s
|
||||
/// split for its reason: the root *must* be rewritten (it carries the title the user just typed),
|
||||
/// so a template whose own `index.md` cannot be edited in place refuses the create — while a
|
||||
/// nested card that is readable-but-uneditable is copied byte-verbatim and simply not stamped,
|
||||
/// because failing a whole create over one hand-dropped flow mapping would be hostile. Its stale
|
||||
/// `modified-by` surviving is the self-reported-provenance honest limit 01 § Frontmatter already
|
||||
/// acknowledges.
|
||||
/// **An instantiation is a copy transaction**, which is `BoardWriter.copyItem`'s posture since
|
||||
/// 2026-07-29 and for its reason: the whole tree is preflighted for stampability before a single
|
||||
/// folder is reminted, and a template carrying one readable-but-uneditable card refuses the create
|
||||
/// whole, naming that card (01-storage-format.md § Frontmatter: "preflights the entire subtree and
|
||||
/// refuses whole, loudly, naming the offending item — never a partial copy, never a silently
|
||||
/// unstamped descendant").
|
||||
///
|
||||
/// This retired the former root-strict/descendants-lenient split, which copied such a card
|
||||
/// byte-verbatim and skipped its stamp. Two things were wrong with the kindness: an unstamped
|
||||
/// descendant keeps a `modified-by` naming somebody who never touched this board, and — since the
|
||||
/// tracker sever joined the copy contract — a live `remote` claim on an object the new board has
|
||||
/// no relationship with. "Proceed partially, lose a little" is never a verdict (01's leniency
|
||||
/// doctrine).
|
||||
///
|
||||
/// **The preflight runs on the destination, not the template**, deliberately: the copy has already
|
||||
/// applied its top-level exclusions, so `.trash/`'s cards — which are not part of what a template
|
||||
/// instantiates — cannot refuse a create they were never going to appear in. Nothing is lost by
|
||||
/// preflighting a step later, because `instantiate`'s construct-then-clean removes the whole
|
||||
/// destination on any throw (09-templates.md's atomicity).
|
||||
private static func mintIdentitiesAndStamps(
|
||||
at root: URL,
|
||||
title: String,
|
||||
operation: WriteOperation
|
||||
) throws(Failure) {
|
||||
do throws(BoardWriteError) {
|
||||
// Before the remint, so a refusal names folders by the paths the user's template actually
|
||||
// has rather than by minted UUIDs they have never seen.
|
||||
try BoardWriter.checkCopiedDescendantsAreStampable(of: root, operation: operation)
|
||||
|
||||
var materialized: [URL] = []
|
||||
try BoardWriter.remintDescendants(of: root, collecting: &materialized, operation: operation)
|
||||
|
||||
@@ -402,7 +419,10 @@ enum TemplateEngine {
|
||||
// cannot answer, and this write is where a template's kind-less root gains it
|
||||
// (`BoardWriter.updateIndex`'s on-touch backfill; no template migration, by design).
|
||||
try BoardWriter.updateIndex(inItemFolder: root, kind: .board, operation: operation) { document in
|
||||
document.set(FrontmatterKeys.created, to: .date(now))
|
||||
// `.born` restamps `created`; the copy contract also severs the reserved tracker keys,
|
||||
// which at board level is the `remote` a template could have carried in from the board
|
||||
// it was saved from (01 ▸ Identity lifecycle, ruled 2026-07-29).
|
||||
BoardWriter.applyCopyContract(to: &document, stamps: .born, now: now)
|
||||
document.set(FrontmatterKeys.title, to: .string(title))
|
||||
}
|
||||
for folder in materialized {
|
||||
|
||||
Reference in New Issue
Block a user