The decision surface — a refused open becomes a live repair, in place
Phase 3 of the decision surface, completing the card (01 ▸ Malformed input, settled 2026-07-31). An attended open's fail-fast walk transforms the loading window's content into one aggregated surface — never a sheet, never a chain: defects grouped by class, each class stated once with its files listed (Reveal in Finder + Open in Editor per row), a class-level default preselected, per-item override behind a disclosure. Only honest choices: YAML and malformed-schema get Editor + Re-check (Skip below the root); newer-than-app gets Skip alone and blocks the board at the root; the two root repairs — minted index, schema: 1 stamp — are defaults. Repair and Open applies fixes in one store-less write bracket and re-walks: clean proceeds, remainder re-aggregates into the same surface. Cancel and ⌘W retire to welcome's row; restored opens never see the surface at all (OpenOrigin rides the PendingOpen carrier). Skips are per-open consent that rides the session — the store retains the skip set and every reload passes it — and the opened board posts a warning-tone notice naming what was left out, each item's Reveal riding the banner strip's new reveal control. On Pro boards the repair bracket binds its own EchoLedger, heal-marks everything, and the store adopts it before the committer starts, so repairs land as one separate commit authored Lanework Integrity — pinned end to end. Also fixed en route: a retired loading window left its close interception installed and returned false from windowShouldClose forever, blocking quit. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
+85
-15
@@ -190,6 +190,32 @@ public final class ScopedAccess {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OpenOrigin
|
||||
|
||||
/// **Whether a person asked for this board right now** (01-storage-format.md § Malformed input, the
|
||||
/// decision surface, settled 2026-07-31):
|
||||
///
|
||||
/// > It appears on **attended opens only** (welcome click, File ▸ Open…, Finder): restoration
|
||||
/// > failures keep the retire-to-welcome-row landing, and the row's retry click is the attended open
|
||||
/// > that then shows the surface — repair is an attended act, and launch never chains dialogs.
|
||||
///
|
||||
/// So this is not a description of *where* an open came from — it is the one bit that decides what a
|
||||
/// failed one does. A closed two-case vocabulary rather than a `Bool` because the sentence a reader
|
||||
/// needs at the branch is "restored boards retire", not "`isAttended` is false".
|
||||
///
|
||||
/// **Attended is the default everywhere**, and that is load-bearing: welcome's rows, File ▸ Open…,
|
||||
/// the Finder open, Duplicate's follow-on open and the template chooser's are all somebody clicking
|
||||
/// something. Exactly one caller says otherwise — launch restoration (`RestoreBootstrapView`) — which
|
||||
/// makes "did a person ask for this" a question one place answers rather than a flag every call site
|
||||
/// has to get right.
|
||||
public enum OpenOrigin: Sendable, Equatable {
|
||||
/// A person just asked for this board.
|
||||
case attended
|
||||
/// Launch restoration reopening what was open last time. Nobody is waiting on it, and a failure
|
||||
/// lands on welcome's row rather than in a surface.
|
||||
case restored
|
||||
}
|
||||
|
||||
// MARK: - AppModel
|
||||
|
||||
/// The app's one piece of cross-window state: which boards are open, which card windows belong to
|
||||
@@ -595,15 +621,31 @@ public final class AppModel {
|
||||
|
||||
// MARK: Pending opens
|
||||
|
||||
/// The security-scoped URL a board window is about to be built from, stashed between
|
||||
/// `openBoard(at:)` and the host's first appearance.
|
||||
/// Everything `openBoard(at:origin:)` knows that a `BoardWindowRef` cannot carry.
|
||||
///
|
||||
/// The handoff exists because a window value has to be `Codable` and a scoped URL is not a
|
||||
/// string: by the time `BoardWindowHost` receives its `BoardWindowRef` the access token is gone
|
||||
/// unless something carried it across. The host claims it on appear; an unclaimed entry (a window
|
||||
/// that never opened) leaks one scope until quit, which is the cheapest failure available here.
|
||||
/// **One struct rather than two parallel dictionaries** (the shape this replaced was
|
||||
/// `pendingAccess` alone): both facts are stashed by the same call, claimed by the same call, and
|
||||
/// meaningless apart — a window that found an origin but no access, or the reverse, would be a
|
||||
/// bug with no honest reading. Keeping them in one value makes "they are always in step" true by
|
||||
/// construction instead of by two `removeValue`s that must not drift.
|
||||
private struct PendingOpen {
|
||||
/// The security-scoped URL the board will be built from, or `nil` where the open needed no
|
||||
/// scope. See `ScopedAccess`: a `URL` rebuilt from `ref.path` grants nothing.
|
||||
let access: ScopedAccess?
|
||||
/// Whether a person asked for this board — what a failed open branches on (`OpenOrigin`).
|
||||
let origin: OpenOrigin
|
||||
}
|
||||
|
||||
/// What a board window is about to be built from, stashed between `openBoard(at:origin:)` and the
|
||||
/// host's first appearance.
|
||||
///
|
||||
/// The handoff exists because a window value has to be `Codable` and neither of these is a
|
||||
/// string: by the time `BoardWindowHost` receives its `BoardWindowRef` the access token is gone,
|
||||
/// and the origin was never in the ref at all. The host claims both on appear; an unclaimed entry
|
||||
/// (a window that never opened) leaks one scope until quit, which is the cheapest failure
|
||||
/// available here.
|
||||
@ObservationIgnored
|
||||
private var pendingAccess: [BoardWindowRef: ScopedAccess] = [:]
|
||||
private var pendingOpens: [BoardWindowRef: PendingOpen] = [:]
|
||||
|
||||
private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "app-model")
|
||||
|
||||
@@ -677,8 +719,19 @@ public final class AppModel {
|
||||
/// **Called before any scene has appeared, and that's fine.** A cold launch's Finder-open can
|
||||
/// reach here before `windowOpener` is captured; the URL joins `pendingOpenURLs` and this same
|
||||
/// method runs again for it once `captureWindowActions` has something to open it with.
|
||||
public func openBoard(at url: URL) {
|
||||
///
|
||||
/// - Parameter origin: whether a person asked for this board right now (`OpenOrigin`) — the one
|
||||
/// bit a *failed* open branches on (01-storage-format.md § Malformed input: the decision
|
||||
/// surface "appears on attended opens only"). `.attended` by default, which is every caller but
|
||||
/// launch restoration: welcome's rows, File ▸ Open…, the Finder open, the template chooser's
|
||||
/// follow-on, Duplicate's. The default is the rule stated once rather than repeated five times.
|
||||
public func openBoard(at url: URL, origin: OpenOrigin = .attended) {
|
||||
guard let windowOpener else {
|
||||
// **The queue carries no origin, and needs none**: it is reachable only *before any scene
|
||||
// exists*, which is the cold-launch Finder/URL open and nothing else — restoration
|
||||
// captures the window actions as its first act (`RestoreBootstrapView.restore`) and so can
|
||||
// never queue. Everything in here is therefore attended, which is what the replay's
|
||||
// default gives it.
|
||||
pendingOpenURLs.append(url)
|
||||
return
|
||||
}
|
||||
@@ -689,11 +742,22 @@ public final class AppModel {
|
||||
}
|
||||
|
||||
let ref = BoardWindowRef(url: url)
|
||||
stashPendingOpen(for: ref, url: url, origin: origin)
|
||||
windowOpener(id: WindowID.board, value: ref)
|
||||
}
|
||||
|
||||
/// Stashes what the window about to appear will claim — the handoff's write half.
|
||||
///
|
||||
/// A method of its own rather than two lines inside `openBoard(at:origin:)` because that method
|
||||
/// cannot run without SwiftUI's `OpenWindowAction`, which is not a thing a test can construct: the
|
||||
/// carrier would otherwise be the one part of the attendance plumbing with no headless proof, and
|
||||
/// an origin that quietly stopped travelling would look exactly like the app before this
|
||||
/// milestone.
|
||||
func stashPendingOpen(for ref: BoardWindowRef, url: URL, origin: OpenOrigin) {
|
||||
// Replacing a stash for the same ref would strand the old scope; there is no such case today
|
||||
// (an unopened window's ref is not reachable), but stopping the loser is free.
|
||||
pendingAccess.removeValue(forKey: ref)?.stop()
|
||||
pendingAccess[ref] = ScopedAccess(url)
|
||||
windowOpener(id: WindowID.board, value: ref)
|
||||
pendingOpens.removeValue(forKey: ref)?.access?.stop()
|
||||
pendingOpens[ref] = PendingOpen(access: ScopedAccess(url), origin: origin)
|
||||
}
|
||||
|
||||
/// Shows — or focuses — the welcome window. Its own scene id, so this works with no windows at
|
||||
@@ -748,10 +812,16 @@ public final class AppModel {
|
||||
sessions[ref]
|
||||
}
|
||||
|
||||
/// Claims the scoped URL `openBoard(at:)` stashed for this window, or `nil` if it opened by some
|
||||
/// other route. Claiming removes it: the session owns the balance from here.
|
||||
func claimPendingAccess(for ref: BoardWindowRef) -> ScopedAccess? {
|
||||
pendingAccess.removeValue(forKey: ref)
|
||||
/// Claims what `openBoard(at:origin:)` stashed for this window. Claiming removes it: the session
|
||||
/// owns the scope's balance from here.
|
||||
///
|
||||
/// A window that opened by some other route — a route that never went through `openBoard` — gets
|
||||
/// no scope and reads as **attended**, which is the safe direction: the worst an attended reading
|
||||
/// can do to a failed open is offer the user a repair they did not ask for, where the reverse
|
||||
/// would silently retire a board somebody just double-clicked.
|
||||
func claimPendingOpen(for ref: BoardWindowRef) -> (access: ScopedAccess?, origin: OpenOrigin) {
|
||||
guard let pending = pendingOpens.removeValue(forKey: ref) else { return (nil, .attended) }
|
||||
return (pending.access, pending.origin)
|
||||
}
|
||||
|
||||
/// Starts a board's session — the board window's host calls this once its load has succeeded.
|
||||
|
||||
Reference in New Issue
Block a user