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:
2026-08-01 10:52:02 -04:00
parent 0933ac1b01
commit 31fee00c73
17 changed files with 2448 additions and 107 deletions
+279 -16
View File
@@ -86,10 +86,39 @@ struct BoardWindowHost: View {
/// that precedes `start()`.
@State private var recordID: UUID?
/// This open's security-scoped access, claimed in `start()` and held until the session takes it
/// over or the open ends.
///
/// `@State` rather than a local in `start()` because the decision surface outlives that call: a
/// repair *writes into the board*, and a scope released when `start()` returned would be released
/// exactly before the one write that needs it. Every exit balances it the session adopts it,
/// or Cancel and the failure path stop it.
@State private var access: ScopedAccess?
/// Whether a person asked for this board (`OpenOrigin`) claimed beside the access, and read by
/// exactly one branch: what a failed walk does.
@State private var origin: OpenOrigin = .attended
/// The board's URL as this open resolved it the scoped one where there is one. Held for the
/// surface's sake, which re-walks and repairs against it long after `start()` has returned.
@State private var boardURL: URL?
/// **The repair bracket's ledger**, held between Repair and Open's writes and the store that the
/// following walk builds (`BoardRepairRun`, `EchoLedger.adopt`).
///
/// It cannot live anywhere else: the repairs run before a store exists and the receipts have to
/// reach that store's ledger before `beginSession` composes Pro's committer, or the app's own
/// repair commits as `Lanework External`. Cleared once adopted.
@State private var repairLedger: EchoLedger?
@State private var phase: Phase = .opening
private enum Phase {
case opening
/// **The walk refused and a person is looking at it** the decision surface, in the loading
/// window's own content area (01-storage-format.md § Malformed input, settled 2026-07-31:
/// "the loading content transforms in place, never a sheet over a spinner").
case deciding(BoardDecisionSurfaceModel)
case open(BoardStore)
/// The load failed; this window is on its way out and must not try again.
case failed
@@ -122,6 +151,15 @@ struct BoardWindowHost: View {
// when `phase` becomes `.open` a snap, which is what assigning outside `withAnimation`
// means here.
BoardLoadingView(indicator: loading)
case let .deciding(model):
// **In place.** The same content area the spinner was in, with no transition of its own:
// 02's snap, read for the surface that arrives instead of a snapshot.
BoardDecisionSurface(
model: model,
onRepairAndOpen: { repairAndOpen(model) },
onRecheck: { recheck(model) },
onCancel: { cancelDecision(model, closingWindow: false) }
)
case .failed:
// Nothing to render and nothing worth animating: this window is dismissing itself.
Color.clear
@@ -246,9 +284,13 @@ struct BoardWindowHost: View {
private func start() async {
guard case .opening = phase else { return }
// Claimed even on the failure path: an unclaimed stash is a scope nobody balances.
let access = appModel.claimPendingAccess(for: ref)
let url = access?.url ?? ref.url
// Claimed even on the failure path: an unclaimed stash is a scope nobody balances. The origin
// rides along one claim, one dictionary (`AppModel.claimPendingOpen`).
let claimed = appModel.claimPendingOpen(for: ref)
access = claimed.access
origin = claimed.origin
let url = claimed.access?.url ?? ref.url
boardURL = url
// Record before load (settled, 02 § Per-board app state). `displayName` is omitted an
// existing record's cached title survives untouched, and a brand-new one takes the folder
@@ -263,9 +305,23 @@ struct BoardWindowHost: View {
configureLoadingWindow(recordID: recordID)
loading.begin()
await attemptOpen(url: url, recordID: recordID, skipping: [])
}
/// **One walk, and what it lands in** the loop Repair and Open and Re-check re-enter.
///
/// It is a method rather than the tail of `start()` because the surface's two buttons "re-run the
/// whole walk" (01-storage-format.md § Malformed input) and must land in exactly the places this
/// lands: a clean walk proceeds into the ordinary open, and a walk that still refuses
/// re-aggregates into the *same* surface. Sharing the body is what makes "never a chained second
/// dialog" structural rather than remembered.
///
/// - Parameter skipping: the surface's consented skips, empty on a first attempt. It reaches the
/// walk *and* the store, which retains it for the session (`BoardStore.skippedPaths`).
private func attemptOpen(url: URL, recordID: UUID, skipping: Set<String>) async {
let store: BoardStore
do throws(BoardLoadFailure) {
guard let acquired = try await appModel.storeRegistry.acquireOffMain(url) else {
guard let acquired = try await appModel.storeRegistry.acquireOffMain(url, skipping: skipping) else {
// W landed while the walk was running, and the walk has now finished into a result
// nobody wants (`acquireOffMain`, discard-on-completion). The window is already
// closing and the registry kept nothing, so the only thing left to balance is this
@@ -273,27 +329,39 @@ struct BoardWindowHost: View {
// after the load, so a cancelled open never set one the very reason it lives there.
Self.logger.debug("board open cancelled during its walk")
loading.end()
access?.stop()
releaseAccess()
return
}
store = acquired
} catch {
Self.logger.error("board failed to open: \(error.description, privacy: .public)")
loading.end()
access?.stop()
phase = .failed
appModel.recordLaunchFailure(path: ref.path, message: error.description)
// The record above just changed the registry welcome, about to appear, must not
// render the stale list `AppModel` cached before this open began, or the failure would
// fall through to the unmatched-failures list for want of a row that already exists.
appModel.refreshRecents()
openWindow(id: WindowID.welcome)
dismissWindow(id: WindowID.board, value: ref)
handleWalkFailure(error, url: url, recordID: recordID)
return
}
// **The window retired while this walk was in flight** Cancel (or W) pressed during a
// Re-check, whose walk then landed successfully. The board must not open behind a window that
// has already gone to welcome, and the reference `acquireOffMain` took has to go back or the
// registry would hold a watcher for a board nobody is showing.
if case .failed = phase {
Self.logger.debug("a walk landed after the open was cancelled — releasing it")
appModel.storeRegistry.release(store)
releaseAccess()
return
}
loading.end()
// **The repair's receipts, into the board's own ledger before the session composes**
// (01: "On Pro boards the repairs drop heal-marked receipts and commit separately as one
// repair commit"). `beginSession` is where Pro's committer is built and started, and the
// committer harvests the ledger it is handed; receipts adopted after that line would be
// receipts the repair commit never sees, and the app's own repair would be authored
// `Lanework External`.
if let repairLedger {
store.echoes.adopt(repairLedger)
self.repairLedger = nil
}
// The load succeeded the frontmatter can be trusted now, so it replaces whatever
// provisional or stale name the record above was carrying. Through `syncDisplayState`,
// deliberately not a second `recordOpen`: this is a display-state refresh, not a second
@@ -307,15 +375,210 @@ struct BoardWindowHost: View {
)
appModel.boardRegistry.setOpenNow(id: recordID)
appModel.beginSession(ref: ref, store: store, recordID: recordID, access: access)
// The session owns the balance from here (`AppModel.beginSession`), so this window must not
// stop it on any later path.
access = nil
phase = .open(store)
configureWindow(store: store, recordID: recordID)
postSkipNoticeIfNeeded(store: store, skipping: skipping)
// "Opening a board from welcome closes welcome" (02 § Launch and window lifecycle). Harmless
// when welcome is not open, which is the ordinary case.
dismissWindow(id: WindowID.welcome)
}
/// **The attendance branch** (01-storage-format.md § Malformed input, settled 2026-07-31): the
/// surface "appears on attended opens only restoration failures keep the retire-to-welcome-row
/// landing".
///
/// Three outcomes, and the third is the new one:
///
/// - **A restored open retires**, exactly as it did before this milestone. Nobody is sitting in
/// front of a launch that reopened four boards, and "launch never chains dialogs".
/// - **An attended open whose failure is environmental retires too** a root that is gone, or a
/// root that is a file. There is nothing on disk to repair, so a surface would offer the user a
/// decision with no choices in it; welcome's row says the same thing in one line, which is
/// where it belonged already. (The carve-out the ruling implies rather than states Redesign
/// Gap 87cd782a.)
/// - **Anything else transforms into the surface**, in place, in this window.
///
/// A surface is *entered* rather than shown: it holds this window's access, its record, and its
/// URL for as long as the user is deciding, which is why none of the three is released here.
private func handleWalkFailure(_ error: BoardLoadFailure, url: URL, recordID: UUID) {
Self.logger.error("board failed to open: \(error.description, privacy: .public)")
// Retired while this walk ran (Cancel during a Re-check): the failure has already been
// recorded and the window is on its way out. A second `retire` would post the row twice.
if case .failed = phase { return }
guard Self.landing(for: error, origin: origin) == .decide else {
loading.end()
retire(message: error.description)
return
}
// Already deciding: this is a Re-check or a Repair and Open landing, and it re-aggregates in
// place the same model, the same window, no second dialog.
if case let .deciding(model) = phase {
model.reaggregate(error)
model.isWorking = false
return
}
loading.end()
phase = .deciding(BoardDecisionSurfaceModel(failure: error, boardRoot: url))
// W now means Cancel (01: the surface's own exit), replacing the loading half's
// cancel-the-walk closure a slot rather than a branch, `configureWindow`'s posture.
windowController.onCloseRequested = {
guard case let .deciding(model) = phase else { return }
cancelDecision(model, closingWindow: true)
}
}
/// Where a refused walk lands.
enum FailureLanding: Equatable {
/// Welcome, on the board's own recents row today's landing, unchanged.
case retire
/// The decision surface, in this window.
case decide
}
/// **The attendance branch as a pure rule** two facts in, one landing out, provable without a
/// window (`BoardDecisionSurfaceTests`).
///
/// It is a static rather than an `if` inside `handleWalkFailure` because it is the ruling's own
/// sentence and the one thing about this milestone that a regression would make silently wrong:
/// a launch that started showing surfaces would chain dialogs across four restored boards, and an
/// attended open that stopped showing one would look exactly like the app before this milestone.
static func landing(for failure: BoardLoadFailure, origin: OpenOrigin) -> FailureLanding {
guard origin == .attended else { return .retire }
return isEnvironmental(failure) ? .retire : .decide
}
/// **Nothing on disk to repair** the environmental carve-out's predicate.
///
/// A single defect, and that defect is a fact about the *root itself* rather than about a file
/// inside it: `BoardLoader` throws these immediately, before any walk, precisely because there is
/// nothing to walk. The single-defect check is stated rather than assumed the loader's
/// environmental throws are single by construction, and a future aggregate carrying one *among*
/// repairable defects should show the surface, because the rest of it is still actionable.
static func isEnvironmental(_ failure: BoardLoadFailure) -> Bool {
guard failure.defects.count == 1 else { return false }
switch failure.primary.reason {
case .unreadableRoot, .notADirectory:
return true
case .boardRootMissingIndex, .unparseableYAML, .missingSchema, .malformedSchema,
.schemaNewerThanApp, .missingOrder, .malformedOrder:
return false
}
}
// MARK: - The surface's three buttons
/// **Repair and Open**: apply every chosen fix in one write bracket, then re-run the whole walk
/// with the skip set (01-storage-format.md § Malformed input).
///
/// The repairs are store-less by necessity and heal-marked by rule (`BoardRepairRun`); the walk
/// that follows is the ordinary one, so a clean result proceeds into the ordinary open and a
/// dirty one re-aggregates here. A repair that failed does not abort anything the batch stops,
/// the failure shows on the surface, and the walk runs anyway, because "a partial repair simply
/// re-aggregates on the next walk".
private func repairAndOpen(_ model: BoardDecisionSurfaceModel) {
guard let url = boardURL, let recordID, !model.isWorking else { return }
model.isWorking = true
model.repairFailure = nil
let outcome = BoardRepairRun.apply(model.plannedRepairs, boardRoot: url)
model.repairFailure = outcome.failure
// Held for the store the walk below may build see `repairLedger`. Merged with anything an
// earlier pass left, so two rounds of repair both reach the commit.
if let existing = repairLedger {
existing.adopt(outcome.ledger)
} else {
repairLedger = outcome.ledger
}
let walk = Task { await attemptOpen(url: url, recordID: recordID, skipping: model.skipSet) }
openWalk.adopt(walk)
}
/// **Re-check**: re-run the whole walk with the current skip set, changing nothing on disk
/// (01: "a disk changed underneath re-aggregates into the *same* surface with the fresh defect
/// list a clean walk proceeds to the first snapshot").
private func recheck(_ model: BoardDecisionSurfaceModel) {
guard let url = boardURL, let recordID, !model.isWorking else { return }
model.isWorking = true
let walk = Task { await attemptOpen(url: url, recordID: recordID, skipping: model.skipSet) }
openWalk.adopt(walk)
}
/// **Cancel** and W, which means exactly this while the surface is up (01: "**Cancel** aborts
/// the open: the window retires and the board lands row-level on welcome, record-before-load
/// unchanged").
///
/// It is the failure path's own sequence, run against the aggregate the surface was showing: the
/// record was minted before the walk, so the row is already waiting for this message.
///
/// - Parameter closingWindow: true when AppKit asked (W), where the interception has to be
/// released for the close to complete. The button's own press dismisses through SwiftUI.
private func cancelDecision(_ model: BoardDecisionSurfaceModel, closingWindow: Bool) {
// A walk may still be in flight behind the surface (Cancel during a Re-check). Its landing is
// guarded by `phase`, which this sets to `.failed`; cancelling as well means the open is not
// merely ignored but abandoned.
openWalk.cancel()
retire(message: model.failure.description)
if closingWindow {
windowController.closeAfterFlush()
}
}
/// The retire-to-welcome landing, in one place: the sequence a failed restore has always had, and
/// the sequence Cancel now shares with it.
private func retire(message: String) {
releaseAccess()
phase = .failed
// **The interception has to go with the phase.** `windowShouldClose` returns `false` whenever
// a closure is installed that is how the close flush gets its turn so a retired window
// whose closure had nothing left to do would *refuse every close request for the rest of the
// app's life*, quit included. There is nothing to intercept once this window is on its way to
// welcome: no store, no session, no decision.
windowController.onCloseRequested = nil
appModel.recordLaunchFailure(path: ref.path, message: message)
// The record minted before the walk just changed the registry welcome, about to appear,
// must not render the stale list `AppModel` cached before this open began, or the failure
// would fall through to the unmatched-failures list for want of a row that already exists.
appModel.refreshRecents()
openWindow(id: WindowID.welcome)
dismissWindow(id: WindowID.board, value: ref)
}
private func releaseAccess() {
access?.stop()
access = nil
}
/// **The skip notice** (01: "the opened board carries a warning-tone notice naming the skipped
/// items, each with Reveal in Finder").
///
/// Written from the walk's own `LoadWarning.userSkipped` entries rather than from the surface's
/// skip set, deliberately: what the notice owes the user is what actually left the board, and a
/// skip for a defect that repaired itself between the decision and the walk names nothing.
///
/// **Only the open that carried the skips posts it.** A second window onto the same board
/// acquires the store that already exists, whose warnings still describe the first open and a
/// notice repeated per window would be the app reporting one decision twice. `skipping` is
/// non-empty for exactly the open that made the decision.
private func postSkipNoticeIfNeeded(store: BoardStore, skipping: Set<String>) {
guard !skipping.isEmpty else { return }
let root = store.rootURL
let items = store.loadWarnings.compactMap { warning -> RevealTarget? in
guard case let .userSkipped(path) = warning else { return nil }
return RevealTarget(path: path, url: root.appendingPathComponent(path))
}
store.banners.postSkippedOnOpen(items)
}
/// **The half of the wiring a window needs before it has a board** everything here is about
/// the *window*, and nothing here mentions the store, which is exactly the split
/// 02-architecture.md's loading state forces: this runs before the walk, and