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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user