Stand up the window architecture — welcome, board, card

Four scenes (welcome, restore bootstrap, board group, card group) with
system restoration disabled in favor of the registry's open-now flags:
set when a window actually opens, cleared only on user close, so quit —
and crash — leave exactly the restoration set behind. AppModel joins
windows to sessions (shared store, registry record, card refs, held
security scope); CloseFlushCoordinator pins 02's strict close order as
a seam-injected machine (card sessions end, windows drain, store
flushes, record stamps, teardown) with named slots where m6/m7 flushes
land. HostedWindowController proxies — never replaces — SwiftUI's
window delegate to intercept windowShouldClose for the flush, report
frames, and place saved frames onto live screens. Card windows are
(board path, case-folded card id) values: reopen focuses, and a
snapshot-pure fate function dismisses on delete, tombstone, tombstoned
lane, or cross-board move.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-27 07:47:11 -04:00
parent 61e18c3dfa
commit fccdf56cf4
19 changed files with 2767 additions and 6 deletions
+102
View File
@@ -0,0 +1,102 @@
import Foundation
// MARK: - BoardWindowRef
/// What a board window *is*, as a value: the board's root path.
///
/// `WindowGroup(id:for:)` keys its windows on the presented value, so this type is simultaneously
/// the window's identity and the argument its host opens with which is what makes
/// "**one board window per root**" (02-architecture.md § Windows) a property of the scene rather
/// than bookkeeping somebody has to remember: `openWindow(value:)` with a ref that already has a
/// window focuses that window instead of opening a second one.
///
/// **A path, not a bookmark or a file identity**, even though the app keys boards by identity
/// everywhere else (`BoardStoreRegistry`, `BoardRegistry`). Two reasons, both about what a window
/// value has to be: it must be `Codable` into a scene-restoration archive, and it must be cheap to
/// compare a `FileIdentity` is neither. The identity keying is not lost, only moved: `AppModel`
/// asks `BoardStoreRegistry.liveStore(for:)` which *is* identity-keyed before opening anything,
/// so a board reached through two spellings of its path still lands on the window it already has.
public struct BoardWindowRef: Codable, Hashable, Sendable {
/// The board root's filesystem path.
public let path: String
public init(path: String) {
self.path = path
}
public init(url: URL) {
self.init(path: url.path)
}
/// The root as a URL again. Carries **no** security-scoped access the scope belongs to the
/// URL object the bookmark resolved to, which the session holds for its whole life (`AppModel`),
/// never to a URL rebuilt from a string.
public var url: URL {
URL(fileURLWithPath: path, isDirectory: true)
}
}
// MARK: - CardWindowRef
/// What a card window is: **board root path plus card GUID** (05-card-window.md Deletion &
/// lifecycle).
///
/// That compound key is the whole lifecycle rule in one value. Within its board the window *follows*
/// its card the key names the card, not the lane, so a move between lanes is invisible to the
/// window. Across boards it does not: "a cross-board move dismisses it exactly like a delete, since
/// the board half of the key no longer names it once the card has left" the card's UUID travels
/// with the move, but `(oldBoard, uuid)` names nothing afterwards, so the window that was keyed on
/// it has no card and dismisses.
///
/// ### The card id is a string, compared like an `ItemID`
///
/// `rawValue` is stored, because the folder's exact spelling is what builds URLs and what must
/// round-trip byte-perfect (`ItemID`'s doc comment in `BoardModel.swift`). But equality and hashing
/// **case-fold** it, because `ItemID` does: two case-spellings of one UUID are one identity
/// everywhere in this app, and a window key that disagreed would open a *second* window for a card
/// whose folder was spelled `ABC` where the first was spelled `abc` the exact duplicate the
/// identity rule exists to prevent. The board half is compared verbatim: it is a path, and paths are
/// the filesystem's business, not this type's.
public struct CardWindowRef: Codable, Hashable, Sendable {
/// The owning board root's path the same string a `BoardWindowRef` carries, which is what lets
/// a card window find its board's session.
public let boardPath: String
/// The card's `ItemID.rawValue`: the folder name exactly as it is spelled on disk.
public let cardID: String
public init(boardPath: String, cardID: String) {
self.boardPath = boardPath
self.cardID = cardID
}
public init(board: BoardWindowRef, cardID: ItemID) {
self.init(boardPath: board.path, cardID: cardID.rawValue)
}
/// The board half, as the board window's own key how a card window reaches its session.
public var board: BoardWindowRef {
BoardWindowRef(path: boardPath)
}
public var boardURL: URL {
board.url
}
/// The card id under `ItemID`'s comparison rule. Computed, so `cardID` stays the single source of
/// truth for what is on disk.
public var cardIdentity: ItemID {
ItemID(rawValue: cardID)
}
public static func == (lhs: CardWindowRef, rhs: CardWindowRef) -> Bool {
lhs.boardPath == rhs.boardPath && lhs.cardIdentity == rhs.cardIdentity
}
public func hash(into hasher: inout Hasher) {
hasher.combine(boardPath)
hasher.combine(cardIdentity)
}
}