Home app-side state in the shared App Group container

Every edition declares group.dev.rzen.indie.Kanban and homes its
app-side state there from day one (12-editions.md ruling 2026-07-29):

- AppGroup namespace: container resolution with per-edition fallback
  when unprovisioned, shared UserDefaults suite, edition identity, and
  a unit-test-host redirect (the test host IS the app — its launch
  sweep and recents refresh must not touch the real shared container).
- BoardRecord: bookmark/isOpenNow replaced by per-edition grants and
  openNow keyed by bundle id; hand-written Codable keeps legacy keys
  decoding (adopted in memory as the running edition's slots, upgraded
  on first save); every other field stays common.
- RecentBoard gains needsReopen: no grant of ours but somebody's —
  first click runs an open panel pre-anchored at the recorded path,
  prompt "Grant"; recordOpen mints this edition's slot onto the
  matched shared record (path fallback only after identity fails and
  only against records holding no grant of ours, so re-granting never
  forks the record).
- Cross-edition freshness: stat-cheap mtime+size stamp re-reads the
  registry when the sibling edition wrote it, so one edition's save
  never erases the other's records wholesale.
- restorables() filters on this edition's open-now flags; the board
  popover gains BoardEditionPresence ("Also open in Lanework Pro"),
  pid-liveness-checked so crash residue never lies.
- Clipboard staging store moves to the group container; the sweep
  claims doomed trees by atomic rename into .sweeping/ then deletes,
  so the sibling's concurrent sweep is a non-event.
- Template store re-homed to the group container per the 09-templates
  re-ruling; scalars (quick-style recents, window size) move to the
  shared suite.
- verify-editions.sh: 30 checks (each edition carries exactly the
  family group). No pathfinder 1.x migrator: 1.x predates the
  registry; state starts fresh in the group container.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-29 20:18:15 -04:00
parent a99e1a52f0
commit 566deab506
28 changed files with 1733 additions and 181 deletions
+18 -14
View File
@@ -95,7 +95,7 @@ enum TemplateEngine {
// MARK: - Where templates live
/// The store folder's name in both locations the bundle's and Application Support's.
/// The store folder's name in both locations the bundle's and the App Group container's.
static let storeFolderName = "Templates"
/// The bundled store: `<app bundle>/Contents/Resources/Templates/`, holding one board folder per
@@ -105,23 +105,27 @@ enum TemplateEngine {
Bundle.main.resourceURL?.appendingPathComponent(storeFolderName, isDirectory: true)
}
/// The user store: `<Application Support>/<bundle id>/Templates/`, beside the board registry and
/// the clipboard staging directory 09's settled location ("Application Support inside the
/// app container friction-free sandbox writes, no location ceremony"), spelled the way every
/// other app-wide store in this app spells it (`ClipboardStore.defaultStagingRoot`,
/// `BoardRegistry`; 02-architecture.md § Per-board app state, "App-wide state has the same home").
/// The user store: `Templates/` in the **shared App Group container**, beside the board registry
/// and the clipboard's staging store (09-templates.md Save as Template Storage, re-homed
/// 2026-07-29; 02-architecture.md § Per-board app state, "App-wide state has the same home").
///
/// > **templates cross editions**: a template saved in base appears in Pro's chooser, honoring 12's
/// > never-an-empty-home-screen promise (templates are plain board folders no per-edition
/// > semantics, no bookmark grant ceremony; the group container is directly writable by every
/// > edition).
///
/// That last clause is why this store needs none of the machinery the registry does: a template is
/// a folder inside a container both editions can write, so there is no bookmark to mint and nothing
/// to grant the cross-sandbox caveat that gives `BoardRecord` its per-edition grant slots simply
/// does not arise. Spelled through `AppGroup.stateDirectory` like every other app-wide store
/// (`ClipboardStore.defaultStagingRoot`, `BoardRegistry.defaultStorageURL`), so all three move
/// together if the home ever does.
///
/// **Named, never created here.** Discovery of a store that does not exist is an empty list, not
/// a directory the app made on the off-chance: the store is minted by the first Save as Template,
/// and by Reveal in Finder, both of which are 09's other cards.
static var userStore: URL {
let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
.appendingPathComponent("Library/Application Support", isDirectory: true)
let bundleIdentifier = Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Kanban"
return support
.appendingPathComponent(bundleIdentifier, isDirectory: true)
.appendingPathComponent(storeFolderName, isDirectory: true)
AppGroup.stateDirectory.appendingPathComponent(storeFolderName, isDirectory: true)
}
// MARK: - Discovery
@@ -445,7 +449,7 @@ enum TemplateEngine {
///
/// **The store's two minters are Save as Template and Reveal in Finder** (see `userStore`, which
/// only names it): a store that exists because the app made it on the off-chance would be an
/// empty folder in Application Support for a user who never used the feature, while a Reveal
/// empty folder in the shared container for a user who never used the feature, while a Reveal
/// that opened nothing or a save that failed because its own home was missing would be the
/// app being pedantic about a directory it owns.
@discardableResult