Remove the App Group wholesale — one sandbox, one bookmark, one flag
Phase 2 of the one-app pivot (DESIGN 12 ▸ App-side state, re-ruled
2026-07-30; reworks 566deab). AppGroup retires; what remains is
AppStateHome — ordinary sandbox Application Support as the one home for
the registry, clipboard staging and template stores, keeping the
unit-test-host redirect (the test host is the app and would sweep real
state). Scalar defaults return to UserDefaults.standard.
BoardRecord's per-edition grant slots and openNow flags collapse to one
bookmark + one isOpenNow; the legacy-key decode and adopt-in-memory
paths go (nothing shipped with group-era records), while the founding
four-keys-required / defaults-for-everything-since decode policy stays —
a bookmarkless record decodes as the born-orphan row rather than
quarantining the list. needsReopen and the pre-anchored re-grant panel
are removed whole: the only state that flow served — a record granted by
a sibling sandbox — is unrepresentable now, and a dead bookmark of our
own was already the orphan case by explicit comment. The
indexOfRecord path fallback dies with it; path is never a key again.
The cross-process freshness stamp (mtime+size re-read) and
BoardEditionPresence with its popover "Also open in…" line retire; the
clipboard prune keeps its atomic .sweeping/ claim-then-delete, reframed
for crash residue and open -n copies rather than sibling editions. The
application-groups entitlement key is gone.
1880 tests in 317 suites green (13 cross-edition tests retired with
their subject).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -12,23 +12,12 @@ import os
|
||||
///
|
||||
/// The pasteboard carries a small JSON manifest plus a plain-text rendering; the *content* — whole
|
||||
/// 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 **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).
|
||||
///
|
||||
/// **⚠ one-app collapse phase 2**: the paragraph below describes a sharing arrangement that stops
|
||||
/// existing when the App Group does (12-editions.md ▸ App-side state, re-ruled 2026-07-30) — the
|
||||
/// staging store moves to the ordinary sandbox container and the sibling it tolerates is only ever
|
||||
/// the developer's own second copy. The tolerance itself is worth keeping either way.
|
||||
///
|
||||
/// **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
|
||||
/// lifecycle below is unchanged by that — both editions read the same machine-wide pasteboard, so both
|
||||
/// sweeps compute the same keep set — with one property made explicit: the sweep tolerates the sibling
|
||||
/// sweeping alongside it (`prune`).
|
||||
/// `<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 **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 staging lifecycle, settled
|
||||
///
|
||||
@@ -115,24 +104,16 @@ public final class ClipboardStore {
|
||||
|
||||
private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "clipboard")
|
||||
|
||||
/// `<group container>/Library/Application Support/Clipboard/`, beside the board registry — the
|
||||
/// same home, for the same reason, and now the same *shared* home (12-editions.md ▸ Both editions
|
||||
/// installed, ruled 2026-07-29):
|
||||
///
|
||||
/// > The clipboard staging store homes in the group container beside the registry, so ⌘C in one
|
||||
/// > 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. 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.
|
||||
/// `<Application Support>/Clipboard/`, beside the board registry — the same home, for the same
|
||||
/// reason (`AppStateHome`; 02-architecture.md § Per-board app state, "App-wide state has the same
|
||||
/// home").
|
||||
public static var defaultStagingRoot: URL {
|
||||
AppGroup.stateDirectory.appendingPathComponent("Clipboard", isDirectory: true)
|
||||
AppStateHome.directory.appendingPathComponent("Clipboard", isDirectory: true)
|
||||
}
|
||||
|
||||
/// The app builds one of these with the system pasteboard and the real staging directory; a test
|
||||
/// passes its own of each, for the reason `BoardRegistry` takes a storage URL at all — injecting
|
||||
/// them is how a suite stays out of the shared App Group container *and* off the machine's one
|
||||
/// them is how a suite stays out of the real Application Support home *and* off the machine's one
|
||||
/// pasteboard.
|
||||
///
|
||||
/// **The launch sweep is here** (04: "a sweep at launch and on each copy"): a fresh store reads
|
||||
@@ -533,21 +514,17 @@ public final class ClipboardStore {
|
||||
/// has no isolation to need.
|
||||
private nonisolated static let sweepFolderName = ".sweeping"
|
||||
|
||||
/// The sweep, written to be safe against **the sibling edition sweeping the same directory at the
|
||||
/// same time** (12-editions.md ▸ Both editions installed: "keep the sweep tolerant of the sibling
|
||||
/// app's concurrent sweep — atomic removals, missing-entry = already swept").
|
||||
/// The sweep, written **claim-then-delete** rather than delete-in-place.
|
||||
///
|
||||
/// The staging root is now shared by every installed edition, and each edition sweeps on its own
|
||||
/// launches, activations, copies and pastes. Both compute the *same* answer — the keep set is the
|
||||
/// one `copyID` the machine-wide pasteboard names — so they never disagree about what should go;
|
||||
/// what they can do is arrive at the same doomed tree together. Two properties make that a
|
||||
/// non-event:
|
||||
/// There is one app and macOS runs one instance of it, so this is not the concurrency guard it was
|
||||
/// written as (12-editions.md ▸ App-side state, re-ruled 2026-07-30 — there is no sibling app to
|
||||
/// race). It is kept because what it buys is cheap and still true of one process:
|
||||
///
|
||||
/// 1. **The claim is a rename, and a rename is atomic.** `moveItem` into `.sweeping/` either
|
||||
/// happens or does not; exactly one sweeper can win it, and the loser's failure is the signal
|
||||
/// that somebody else owns the tree now. Deleting in place would instead have two processes
|
||||
/// walking one directory tree as it disappeared under them — the case where a half-removed tree
|
||||
/// is briefly *visible*, which is the only way a concurrent sweep could corrupt a paste.
|
||||
/// 1. **The claim is a rename, and a rename is atomic.** A tree either leaves the staging root
|
||||
/// whole or stays there whole — it is never briefly *visible half-removed*, which is the one
|
||||
/// state a reader could misread. That covers a crash mid-delete, and it covers the developer's
|
||||
/// own second copy launched with `open -n`, which shares this container because it is the same
|
||||
/// app.
|
||||
/// 2. **A missing entry means already swept, never an error.** Every failure here is swallowed:
|
||||
/// the listing is stale by the time it is walked, and a tree that vanished between the two is
|
||||
/// precisely the outcome asked for.
|
||||
@@ -573,8 +550,8 @@ public final class ClipboardStore {
|
||||
}
|
||||
let claim = sweepFolder.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
guard (try? FileManager.default.moveItem(at: entry, to: claim)) != nil else {
|
||||
// Gone, or the sibling's sweep claimed it first. Either way it is not ours to delete
|
||||
// and nothing is wrong.
|
||||
// Gone, or claimed by another pass. Either way it is not ours to delete and nothing
|
||||
// is wrong.
|
||||
continue
|
||||
}
|
||||
claimed.append(claim)
|
||||
@@ -584,9 +561,8 @@ public final class ClipboardStore {
|
||||
try? FileManager.default.removeItem(at: claim)
|
||||
}
|
||||
|
||||
// Anything a previous pass claimed and did not finish — including the sibling app's, whose
|
||||
// claims are as much ours to collect as our own, since a claimed tree is unreachable by
|
||||
// either. Best-effort, and an empty or missing folder is nothing to do.
|
||||
// Anything a previous pass claimed and did not finish — a crash between the claim and the
|
||||
// delete. Best-effort, and an empty or missing folder is nothing to do.
|
||||
if let stragglers = try? FileManager.default.contentsOfDirectory(
|
||||
at: sweepFolder,
|
||||
includingPropertiesForKeys: nil,
|
||||
|
||||
Reference in New Issue
Block a user