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:
@@ -40,10 +40,18 @@ struct WelcomeRow: Identifiable, Equatable {
|
||||
let icon: String?
|
||||
let iconColor: String?
|
||||
|
||||
/// Where the board is **now**, or `nil` when its bookmark no longer resolves. The single source
|
||||
/// Where the board is **now**, or `nil` when this edition cannot reach it. The single source
|
||||
/// of the row's availability: Open and Reveal need a URL, and an orphan has none.
|
||||
let url: URL?
|
||||
|
||||
/// Where the re-grant panel starts for a row whose only grant another edition minted
|
||||
/// (`RecentBoard.needsReopen`; 12-editions.md ▸ Distribution) — `nil` on every other row.
|
||||
///
|
||||
/// It is what makes this row's Open live while `url` is `nil`: the board is *there*, and one click
|
||||
/// plus Grant is all that stands between the user and it. Reveal in Finder stays disabled, because
|
||||
/// revealing a folder is a read this app has not been granted either.
|
||||
let regrantAnchor: URL?
|
||||
|
||||
/// The containing folder, for the row's location line — Xcode's welcome shows where a project
|
||||
/// lives, not its own path repeated under its name. Home-abbreviated where it can be.
|
||||
let location: String
|
||||
@@ -58,10 +66,14 @@ struct WelcomeRow: Identifiable, Equatable {
|
||||
|
||||
var isAvailable: Bool { url != nil }
|
||||
|
||||
/// Open and Reveal in Finder both need somewhere to go; Forget is deliberately not here, because
|
||||
/// it is enabled on every row — an orphan the user can never open is exactly the row that most
|
||||
/// needs erasing (02 § Graceful orphaning: "recents surface it as unavailable with Forget").
|
||||
var canOpen: Bool { isAvailable }
|
||||
/// Whether this row is waiting for this edition's grant rather than being genuinely orphaned.
|
||||
var needsReopen: Bool { regrantAnchor != nil }
|
||||
|
||||
/// Open needs somewhere to go **or something to grant**; Reveal in Finder needs the former only.
|
||||
/// Forget is deliberately not here, because it is enabled on every row — an orphan the user can
|
||||
/// never open is exactly the row that most needs erasing (02 § Graceful orphaning: "recents
|
||||
/// surface it as unavailable with Forget").
|
||||
var canOpen: Bool { isAvailable || needsReopen }
|
||||
var canReveal: Bool { isAvailable }
|
||||
|
||||
/// The row's third line — one line, so the three states are alternatives rather than a stack.
|
||||
@@ -74,12 +86,20 @@ struct WelcomeRow: Identifiable, Equatable {
|
||||
case counts(lanes: Int?, cards: Int?)
|
||||
/// The bookmark no longer resolves (02 § Graceful orphaning).
|
||||
case unavailable
|
||||
/// This edition has never been granted the board another edition minted the record for
|
||||
/// (12-editions.md ▸ Distribution) — the *reopen* state, which is not orphaning: the board is
|
||||
/// there, and one click opens the panel that grants it.
|
||||
case needsReopen
|
||||
/// Fail-fast's specifics, from the open or restore that failed.
|
||||
case failed(String)
|
||||
}
|
||||
|
||||
var caption: Caption {
|
||||
if let failure { return .failed(failure) }
|
||||
// Before `unavailable`, because the two are told apart by *why* there is no URL and this one
|
||||
// is the reachable case: an orphan's caption offering to grant access would be a promise the
|
||||
// app cannot keep, and this row wearing the orphan's caption would read as a loss it is not.
|
||||
if needsReopen { return .needsReopen }
|
||||
if url == nil { return .unavailable }
|
||||
return .counts(lanes: laneCount, cards: cardCount)
|
||||
}
|
||||
@@ -143,6 +163,7 @@ struct WelcomeRow: Identifiable, Equatable {
|
||||
icon: record.icon,
|
||||
iconColor: record.iconColor,
|
||||
url: recent.url,
|
||||
regrantAnchor: recent.regrantAnchor,
|
||||
location: location(of: recent.url?.path ?? record.lastKnownPath),
|
||||
laneCount: record.laneCount,
|
||||
cardCount: record.cardCount,
|
||||
|
||||
Reference in New Issue
Block a user