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
+54
View File
@@ -0,0 +1,54 @@
import Foundation
import Testing
@testable import Kanban
/// **Where app-side state lives** (`AppStateHome`; 02-architecture.md § Per-board app state,
/// "App-wide state has the same home"; 12-editions.md App-side state, re-ruled 2026-07-30 one
/// app, one sandbox, one home).
///
/// Two claims, and they are the only two this type makes: the three file stores are neighbours under
/// one directory the app can actually create, and a **unit-test host never resolves to the real
/// one**. The second is not a nicety the test host *is* the app, so `KanbanApp.init()` runs for
/// real on every test launch, and a home that pointed at the developer's own state would have the
/// host's launch sweep collecting real staged clipboard trees and its recents refresh rewriting real
/// records. There is no injection point in `App.init` to fix that from outside.
@MainActor
@Suite("App state home")
struct AppStateHomeTests {
@Test("Every app-side store resolves under one state directory, and it is creatable")
func stateDirectoryIsOneHomeAndAlwaysUsable() throws {
let home = AppStateHome.directory
// The registry, the clipboard's staging store and the template store are one another's
// neighbours by design, and each names the home rather than spelling a path so this is the
// assertion that keeps them moving together the day the home moves.
#expect(BoardRegistry.defaultStorageURL.deletingLastPathComponent() == home)
#expect(ClipboardStore.defaultStagingRoot.deletingLastPathComponent() == home)
#expect(TemplateEngine.userStore.deletingLastPathComponent() == home)
// And it is a directory the app can actually create, which is the one property that has to
// hold whichever side of the test-host redirect this is running on.
try FileManager.default.createDirectory(at: home, withIntermediateDirectories: true)
#expect(FileManager.default.fileExists(atPath: home.path))
}
@Test("A unit-test host never resolves to the real Application Support home")
func aTestHostIsRedirected() {
// This suite is running, so this *is* a test host and the point of the check is that the
// two defaults every launch reaches for (the registry file, the staging root) cannot land in
// the home the developer's own running copy uses.
#expect(AppStateHome.isUnitTestHost)
#expect(AppStateHome.directory == AppStateHome.unitTestDirectory)
#expect(AppStateHome.directory != AppStateHome.productionDirectory)
}
@Test("The production home is Application Support itself, with no bundle-id subfolder appended")
func productionHomeHasNoBundleIDSubfolder() {
// The sandbox already scopes `Application Support` to this app that container path is the
// one place the bundle id belongs so a subfolder appended *inside* it would name the app
// twice. The last component is therefore the check: what this type adds is nothing.
#expect(AppStateHome.productionDirectory.lastPathComponent == "Application Support")
}
}