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
+6 -107
View File
@@ -68,15 +68,6 @@ struct BoardInfoWidget: View {
let store: BoardStore
let recents: StyleRecents
/// The cross-edition awareness line's *fact*, asked afresh each time the popover is built see
/// `BoardEditionPresence` for why it is a closure rather than a value: both halves of it (the
/// registry's flags and whether the other app is alive) can change while a board window sits
/// there, and neither is a thing to observe.
///
/// Defaulted to "no line" so the two surfaces that build this widget without a registry the
/// titlebar tests keep saying nothing rather than needing one.
var otherEditionNote: () -> String? = { nil }
@Bindable var presentation: BoardInfoPresentation
var body: some View {
@@ -96,7 +87,7 @@ struct BoardInfoWidget: View {
.help("Board Info")
.accessibilityLabel("Board Info")
.popover(isPresented: $presentation.isPresented, arrowEdge: .bottom) {
BoardInfoView(store: store, recents: recents, otherEditionNote: otherEditionNote())
BoardInfoView(store: store, recents: recents)
}
}
}
@@ -112,14 +103,12 @@ struct BoardInfoWidget: View {
func boardInfoTitlebarAccessory(
store: BoardStore,
recents: StyleRecents,
presentation: BoardInfoPresentation,
otherEditionNote: @escaping () -> String? = { nil }
presentation: BoardInfoPresentation
) -> NSTitlebarAccessoryViewController {
let hosting = NSHostingView(
rootView: BoardInfoWidget(
store: store,
recents: recents,
otherEditionNote: otherEditionNote,
presentation: presentation
)
)
@@ -152,10 +141,6 @@ struct BoardInfoView: View {
/// See `BoardGitNote.hasGitDirectory(at:)` for why a live-updating fact isn't needed here.
private let hasGitDirectory: Bool
/// The cross-edition awareness line, or `nil` for no line resolved once when the view is built,
/// on `hasGitDirectory`'s terms and for its reason (`BoardEditionPresence`).
private let otherEditionNote: String?
/// The style editor brings its own padding, so the sections around it carry the same number by
/// hand instead of an outer padding that would double up on it **the editor's own figure**
/// (`StyleEditorLayout.sectionSpacing`), which is font-derived, so the popover's chrome scales
@@ -164,11 +149,10 @@ struct BoardInfoView: View {
StyleEditorLayout.sectionSpacing(bodyPointSize: CardWindowMetrics.bodyPointSize)
}
init(store: BoardStore, recents: StyleRecents, otherEditionNote: String? = nil) {
init(store: BoardStore, recents: StyleRecents) {
self.store = store
self.recents = recents
self.hasGitDirectory = BoardGitNote.hasGitDirectory(at: store.rootURL)
self.otherEditionNote = otherEditionNote
}
var body: some View {
@@ -196,21 +180,10 @@ struct BoardInfoView: View {
// nothing here at all no header, no divider, no placeholder and the popover ends at
// Styling, complete in itself. Only a board that actually carries an inert `.git` earns
// this closing note.
//
// The awareness line is its neighbour on the same terms, and the two stack in one section
// when both are true a `.git`-bearing board open in Pro is exactly the household where
// both sentences apply, and each answers a different question.
if hasGitDirectory || otherEditionNote != nil {
if hasGitDirectory {
Divider()
VStack(alignment: .leading, spacing: 4) {
if hasGitDirectory {
BoardGitNote()
}
if let otherEditionNote {
BoardEditionPresenceNote(text: otherEditionNote)
}
}
.padding(inset)
BoardGitNote()
.padding(inset)
}
}
// The style editor's popover width, taken from the editor rather than restated: the embed
@@ -344,77 +317,3 @@ struct BoardGitNote: View {
FileManager.default.fileExists(atPath: boardRoot.appendingPathComponent(".git").path)
}
}
// MARK: - The other edition
/// ** Retired by the one-app collapse one-app collapse phase 2.** There is no other edition to be
/// aware of: 12-editions.md App-side state (re-ruled 2026-07-30) removes the App Group and with it
/// the cross-edition flags this line reads, and 12 Distribution retires the second app outright.
/// This type, `BoardInfoView.otherEditionNote` and `BoardRecord.openNow`'s keying go together in that
/// phase; until then the line is simply never non-`nil` in practice, since no sibling exists to set a
/// flag. Kept functioning, and documented as it was built, rather than half-unwound here.
///
/// **The cross-edition awareness line** "Also open in Lanework Pro" (12-editions.md Both editions
/// installed, ruled 2026-07-29).
///
/// > Two conveniences ride the shared registry: open-now flags are per-edition , and the board
/// > popover carries a contextual awareness line ("Also open in Lanework Pro") read from the other
/// > edition's flag, **pid-liveness-checked so crash residue never lies** a line, never a gate.
///
/// It is `BoardGitNote`'s posture applied to a different fact: contextual, absent when untrue, and
/// never a control. Nothing about it gates anything the same board open in both apps is the designed
/// foreign-writer story (12), so this line exists to *explain* what the user is looking at, not to
/// warn them off it.
///
/// ### Why liveness matters, and why it is a parameter
///
/// The flag is a live open marker that a crash deliberately leaves standing (02-architecture.md
/// § Launch and window lifecycle that residue is what makes crash recovery free). So a flag alone
/// would claim "also open in Lanework Pro" about an app that died last Tuesday. `NSRunningApplication`
/// settles it, and the check is injected so `note(otherEditions:isRunning:)` is a pure function the
/// tests pin directly the same seam `BoardGitNote.hasGitDirectory(at:)` is.
enum BoardEditionPresence {
/// Whether an edition is running right now, by bundle id the pid-liveness half.
///
/// `NSRunningApplication.runningApplications(withBundleIdentifier:)` rather than a stored pid: the
/// registry records *which* edition had the board open, never a process id, and it should not
/// start a pid is a fact with a shelf life of milliseconds, and the bundle id is the question
/// actually being asked.
@MainActor
static func isRunning(_ bundleID: String) -> Bool {
!NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).isEmpty
}
/// The line, or `nil` for no line at all.
///
/// Three ways to get `nil`, and each is the honest answer: no other edition has the board flagged;
/// the flagged edition is not running (crash residue); or the flagged bundle id is one this build
/// cannot name (`AppGroup.editionDisplayName`) a future edition, where inventing a name would be
/// worse than saying nothing.
///
/// **One line even when several editions qualify**, taking the first by the sorted order the
/// registry hands over: the popover has room for a sentence, not a roster, and with Teams
/// deferred there is no case today where two others are open at once.
static func note(otherEditions: [String], isRunning: (String) -> Bool) -> String? {
for bundleID in otherEditions {
guard isRunning(bundleID), let name = AppGroup.editionDisplayName(bundleID) else { continue }
return "Also open in \(name)"
}
return nil
}
}
/// The awareness line as a view, beside `BoardGitNote` and styled identically the two contextual
/// notes are one register, so a popover carrying both reads as one surface.
struct BoardEditionPresenceNote: View {
let text: String
var body: some View {
Text(text)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}