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:
2026-07-30 17:46:32 -04:00
parent 092300c7d2
commit 2c6b8fe63a
27 changed files with 385 additions and 1461 deletions
+88
View File
@@ -0,0 +1,88 @@
import Foundation
/// **Where app-side state lives** the board registry, the clipboard's staging snapshots, the user
/// template store (02-architecture.md § Per-board app state, "App-wide state has the same home").
///
/// ### One app, one sandbox, one home
///
/// There is one application (12-editions.md The target, re-ruled 2026-07-30), so there is one
/// sandbox container and nothing to share state *with*. `Library/Application Support` inside that
/// container is the whole answer: the sandbox already scopes it per app, which is why there is **no
/// bundle-id subfolder** a subfolder inside a container that is already this app's alone would be
/// ceremony naming the app twice.
///
/// Scalars are not here. A window size or a toggle goes to `UserDefaults.standard`
/// (`AppPreferences`), which the sandbox scopes on exactly the same terms; this type is only about
/// the *file* stores.
///
/// ### Why the type exists at all rather than three copies of four lines
///
/// Three stores answer "where do I live" and they must answer it identically: the registry, the
/// clipboard staging root and the template store are neighbours by design, and a test that asserts
/// they are neighbours (`BoardRegistryTests`) is asserting something real. One name for the home is
/// what keeps them moving together the day it moves.
public enum AppStateHome {
/// The directory every app-side file store lives in.
///
/// In a shipped app this is `productionDirectory`. **In a unit-test host it is a scratch
/// directory** (`isUnitTestHost`) see that property for why the default has to move for a test
/// host when everything else about testing is injection.
public static var directory: URL {
isUnitTestHost ? unitTestDirectory : productionDirectory
}
/// What a shipped app uses: `Library/Application Support` inside the sandbox container.
///
/// The `NSHomeDirectory()` fallback covers the case where `FileManager` answers with no domain
/// at all not a state a shipped app is in, but this file must not be the thing that throws.
public static var productionDirectory: URL {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
.appendingPathComponent("Library/Application Support", isDirectory: true)
}
// MARK: - The unit-test host
/// Whether this process is hosting a unit-test bundle.
///
/// ### Why the app has to know
///
/// The unit-test host **is the app** (`KanbanTests` is a hosted bundle), so `KanbanApp.init()`
/// runs for real on every test launch and builds an `AppModel` over whatever the defaults resolve
/// to. Every *object* a test constructs takes its storage by injection that is the seam, and it
/// is untouched but the host's own launch has no injection point, and the thing it reaches for
/// is the developer's own state: its launch sweep would collect real staged clipboard trees, and
/// its `refreshRecents()` would resolve, refresh and rewrite real records.
///
/// So the *default* moves for a test host, which is the one place a default can be wrong in a way
/// injection cannot fix.
///
/// ### Why this variable and not a launch flag
///
/// `UITestLaunch.fixtureFlag` is the flag-shaped answer and remains the right one for the UI
/// suites, which launch the app themselves and can pass arguments. A *unit*-test host is launched
/// by the test runner, which passes nothing of ours but it does set these variables, and it has
/// set them for as long as XCTest has existed. Three spellings are checked because Apple has used
/// each at some point and a missed one would silently mean "not a test".
///
/// It cannot fire in a shipped app: nothing sets these but a test runner.
public static var isUnitTestHost: Bool {
let environment = ProcessInfo.processInfo.environment
return environment["XCTestConfigurationFilePath"] != nil
|| environment["XCTestBundlePath"] != nil
|| environment["XCTestSessionIdentifier"] != nil
}
/// The scratch home a test host uses instead. Inside the app's own container
/// (`NSTemporaryDirectory` sandboxes there), so nothing outside this app can see it and the OS
/// reclaims it.
///
/// One fixed folder rather than one per run: the suites do not depend on it being empty they
/// inject their own paths for anything they assert on and a stable name keeps it inspectable
/// when something writes there that should not have.
public static var unitTestDirectory: URL {
URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent("LaneworkUnitTestState", isDirectory: true)
}
}