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
+9 -9
View File
@@ -91,7 +91,7 @@ struct BoardRegistryTests {
let id = registry.recordOpen(of: fixture.root, displayName: "Todo Board")
let created = try #require(registry.record(id: id))
#expect(!created.bookmark.isEmpty)
#expect(created.grant(forEdition: registry.editionID) != nil)
#expect(created.displayName == "Todo Board")
#expect(created.lastKnownPath == fixture.root.path)
#expect(created.laneCount == nil, "counts are stamped at close, never guessed at open")
@@ -135,7 +135,7 @@ struct BoardRegistryTests {
let id = registry.recordOpen(of: fixture.root)
let record = try #require(registry.record(id: id))
#expect(record.displayName == fixture.root.deletingPathExtension().lastPathComponent)
#expect(!record.bookmark.isEmpty, "the bookmark still mints on the before-load call")
#expect(record.grant(forEdition: registry.editionID) != nil, "the bookmark still mints on the before-load call")
#expect(record.icon == nil)
#expect(record.iconColor == nil)
}
@@ -163,7 +163,7 @@ struct BoardRegistryTests {
#expect(record.icon == "star")
#expect(record.iconColor == "fern")
#expect(record.lastOpened > firstOpened, "the bookmark and lastOpened still refresh on every open attempt")
#expect(!record.bookmark.isEmpty)
#expect(record.grant(forEdition: registry.editionID) != nil)
}
// MARK: Counts
@@ -620,16 +620,16 @@ struct BoardRegistryTests {
let registry = BoardRegistry(storageURL: storage.url)
let id = registry.recordOpen(of: fixture.root, displayName: "Work")
#expect(registry.record(id: id)?.isOpenNow == nil, "recording an open is not opening a window")
#expect(registry.record(id: id)?.isOpen(inEdition: registry.editionID) == false, "recording an open is not opening a window")
#expect(registry.restorables().isEmpty)
registry.setOpenNow(id: id)
#expect(registry.record(id: id)?.isOpenNow == true)
#expect(registry.record(id: id)?.isOpen(inEdition: registry.editionID) == true)
#expect(ids(registry.restorables()) == [id])
// A user close. The flag goes, and with it the board's place in the next launch.
registry.clearOpenNow(id: id)
#expect(registry.record(id: id)?.isOpenNow == false)
#expect(registry.record(id: id)?.isOpen(inEdition: registry.editionID) == false)
#expect(registry.restorables().isEmpty)
// A quit. The teardown stamps counts and does *not* clear the flag that omission is the
@@ -639,7 +639,7 @@ struct BoardRegistryTests {
registry.recordClose(id: id, displayName: "Work", laneCount: 2, cardCount: 5)
let afterRelaunch = BoardRegistry(storageURL: storage.url)
#expect(afterRelaunch.record(id: id)?.isOpenNow == true, "the flags describe what was open at quit")
#expect(afterRelaunch.record(id: id)?.isOpen(inEdition: registry.editionID) == true, "the flags describe what was open at quit")
#expect(ids(afterRelaunch.restorables()) == [id])
#expect(afterRelaunch.record(id: id)?.laneCount == 2)
}
@@ -712,13 +712,13 @@ struct BoardRegistryTests {
let registry = BoardRegistry(storageURL: storage.url)
#expect(registry.recents().count == 1, "the file decoded; nothing was quarantined")
#expect(registry.record(id: id)?.isOpenNow == nil, "a missing key reads as 'not open'")
#expect(registry.record(id: id)?.isOpen(inEdition: registry.editionID) == false, "a missing key reads as 'not open'")
#expect(registry.restorables().isEmpty)
#expect(registry.record(id: id)?.laneCount == 4, "and every other field survived")
// And the key writes through from here on.
registry.setOpenNow(id: id)
#expect(BoardRegistry(storageURL: storage.url).record(id: id)?.isOpenNow == true)
#expect(BoardRegistry(storageURL: storage.url).record(id: id)?.isOpen(inEdition: registry.editionID) == true)
}
// MARK: Bookmarks in a sandboxed host