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
+47 -2
View File
@@ -314,8 +314,17 @@ public enum BoardWriter: Sendable {
/// `index.md`, after it, so the file that makes a folder a board is written first and a failure
/// to seed can never leave a half-made board. Seeding is `seedGitignoreIfAbsent`'s, so a
/// creation into a folder that somehow already carries one leaves it alone.
public static func createBoard(at rootURL: URL, title: String?) throws(BoardWriteError) {
let operation = WriteOperation.createBoard
/// - Parameter operation: what the *caller* was doing, for the banner's sake `.createBoard` for
/// every gesture that makes a board, and `.mintBoardIndex` for the decision surface's repair of
/// a board folder that has everything except the file that says it is one
/// (01-storage-format.md § Malformed input). The mechanics are identical, which is exactly why
/// the repair reuses this method rather than growing a second one; only the sentence a failure
/// would produce differs, and that sentence is the vocabulary's whole job.
public static func createBoard(
at rootURL: URL,
title: String?,
operation: WriteOperation = .createBoard
) throws(BoardWriteError) {
let indexURL = rootURL.appendingPathComponent(BoardLoader.indexFileName)
guard !FileManager.default.fileExists(atPath: indexURL.path) else {
@@ -2952,6 +2961,34 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
/// It never describes an *edit*: the app writes this file only when nothing holds the name.
case seedGitignore
/// **The decision surface's minted board index** (01-storage-format.md § Malformed input, settled
/// 2026-07-31): "*Board root without `index.md`* minted repair, the default: create a board
/// index (folder-name title, `schema: 1`)".
///
/// Its own case rather than a fold into `.createBoard`, on the vocabulary's standing reasoning:
/// nothing is being *created* the board is right there, with its lanes and its cards, and the
/// user opened it so a banner saying the app "couldn't create the board" would name a gesture
/// nobody made and a thing that already exists. What is missing is the one file that says the
/// folder is a board, and that is what this names.
///
/// **No payload**, for `.seedGitignore`'s reason: there is one such file per board, its name is
/// fixed, and the board it belongs to has no title yet reading one is precisely what the
/// missing file prevents.
case mintBoardIndex
/// **The decision surface's `schema: 1` stamp** (01-storage-format.md § Malformed input): "*Board
/// root missing `schema`* stamp `schema: 1`, the default: reliable exactly because the walk just
/// validated the file against schema 1."
///
/// Its own case beside `.mintBoardIndex` rather than a fold into it the no-folding rule the
/// whole vocabulary is built on because the two repairs have different outcomes a user could
/// care about: one writes a file that was not there, the other adds a key to a file that was. A
/// banner saying the app could not create this board's `index.md` when the file is sitting in
/// Finder would be actively wrong.
///
/// **No payload**, for `.mintBoardIndex`'s reason exactly.
case stampSchema
/// A wrong-kinded node being moved off a board-root name the app claims a file or symlink
/// squatting `.trash` (01-storage-format.md § Fractal layout Rules, ruled 2026-07-29: "moved
/// aside by a scheduled heal via the Finder-style rename ladder").
@@ -3074,8 +3111,13 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
// The comment family is identity for `.repairDuplicateID`'s reason, doubled: a comment's
// `index.md` carries no `title` to enrich from, and the title these five hold is the *card's*,
// filled in by the caller from the window the gesture came from.
// The two decision-surface repairs join this list for `.createBoard`'s and
// `.seedGitignore`'s reasons at once: neither carries a title slot, and the board they
// repair has no readable title to enrich from a root with no `index.md` has no document
// at all, and one with no `schema` is the file the walk just refused.
case .createBoard, .createLane, .createCard, .importAttachment, .listAttachments,
.removeAttachment, .renumberChildren, .relocateLooseFile, .agentGuide, .seedGitignore,
.mintBoardIndex, .stampSchema,
.displaceClaimedName, .repairDuplicateID, .saveCommentDraft, .postComment,
.editComment, .deleteComment, .purgeCommentTrash:
self
@@ -3139,6 +3181,7 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case .createBoard, .createLane, .createCard, .move, .copy, .delete, .purge, .migrateTombstone,
.style, .resize, .rename, .duplicateBoard, .saveAsTemplate, .paste, .importAttachment,
.listAttachments, .removeAttachment, .relocateLooseFile, .agentGuide, .seedGitignore,
.mintBoardIndex, .stampSchema,
.displaceClaimedName,
.repairDuplicateID, .toggleTask, .editBody, .rawSource, .saveCommentDraft, .postComment,
.editComment, .deleteComment, .purgeCommentTrash:
@@ -3176,6 +3219,8 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case let .relocateLooseFile(filename): "relocate loose file '\(filename)'"
case .agentGuide: "update the agent guide"
case .seedGitignore: "seed the board's .gitignore"
case .mintBoardIndex: "create this board's index.md"
case .stampSchema: "stamp this board's schema"
case let .displaceClaimedName(name): "move a stray '\(name)' aside"
case let .repairDuplicateID(title): Self.phrase("repair the duplicate id of", title)
case let .toggleTask(title): Self.phrase("toggle a checkbox in", title)