Settle the fail-fast decision surface design

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-31 18:44:41 -04:00
parent bec75e4282
commit 3bd6187b94
2 changed files with 15 additions and 3 deletions
+14 -2
View File
@@ -123,15 +123,27 @@ The tombstone model is **retired** — no `deleted:` key, no in-place flags, no
## Malformed input — fail fast
Loud, specific error (path + what's wrong) for: unparseable YAML, missing required fields (`schema`; `order` where required), `schema` newer than the app, board root without `index.md`. No partial loads. The only tolerated absence is a missing `index.md` below the root (skip + warn, per Rules above). An explicitly null value (`order:` with nothing after it, `order: null`) reads as **missing** (settled): on a required field that is the missing-required-field rejection — it describes what the hand-editor actually did, started the key and never gave it a value; on an optional field, null is simply absent and the default applies.
Loud, specific error (path + what's wrong) for: unparseable YAML, missing required fields (`schema`; `order` where required), `schema` newer than the app, board root without `index.md`. No partial loads — the only partial load is the decision surface's user-chosen Skip (below), and the only tolerated absence is a missing `index.md` below the root (skip + warn, per Rules above). An explicitly null value (`order:` with nothing after it, `order: null`) reads as **missing** (settled): on a required field that is the missing-required-field rejection — it describes what the hand-editor actually did, started the key and never gave it a value; on an optional field, null is simply absent and the default applies.
Fail-fast is the **initial-load** contract. Once a board is open, a failed live reload does not blank the board: the window keeps the last good snapshot and surfaces the same loud specifics in a non-modal banner — see 02-architecture.md's live-reload resilience.
**The decision surface (settled 2026-07-31).** Blocking means one aggregated surface hosted by the pre-snapshot loading window (02-architecture.md ▸ the loading state) — the loading content transforms in place, never a sheet over a spinner. 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. The loader collects every fail-fast defect in the walk rather than stopping at the first. The surface groups defects **by class**: each class section states the defect once, lists the affected files (path + specifics, each with Reveal in Finder and Open in Editor), and carries one class-level choice preselected to its default, with per-item override behind a disclosure. The vocabulary offers only honest choices per class:
- *Unparseable YAML* — no app-minted rewrite (the app would be guessing at content): Open in Editor + Re-check, or Skip.
- *Missing/null/malformed `order`* — minted lossless repair, the default: place at the bottom of its container (max + 1024). Order is app-owned; nothing user-authored is lost.
- *Missing `schema`* — stamp `schema: 1`, the default: reliable exactly because the walk just validated the rest of the file against schema 1.
- *`schema` newer than the app* — no honest fix (downgrading risks silent loss): an unfixable row ("made by a newer Lanework — update the app") offering only Skip; on the board root it blocks the whole board (Cancel is the only exit).
- *Board root without `index.md`* — minted repair, the default: create a board index (folder-name title, `schema: 1`) — the user just opened this folder as a board, and the mint is content-lossless.
**Skip is user-consented tolerance, loudly marked** (ruled 2026-07-31): a skipped item loads the board without it — the file stays on disk untouched, tolerated-invisible like strays — and the opened board carries a warning-tone notice naming the skipped items, each with Reveal in Finder. Skips are **per-open decisions, never persisted**: the next open of a still-broken board presents the surface again — the notice is the honest residue of this open, not a stored preference.
Buttons: **Repair and Open** applies every chosen fix in one write bracket — each repaired `index.md` is an ordinary app write (stamps `modified`, clears `modified-by`); interrupted batches are accepted per the renumber precedent, every intermediate state valid, and a partial repair simply re-aggregates on the next walk. On Pro boards the repairs drop heal-marked receipts and commit separately as one repair commit, never folded into anyone else's work (Validation and healing below ▸ Healing mutations commit separately). **Cancel** aborts the open: the window retires and the board lands row-level on welcome, record-before-load unchanged. **Re-check** — and Repair and Open itself — re-runs the whole walk: a disk changed underneath re-aggregates into the *same* surface with the fresh defect list, never a chained second dialog; a clean walk proceeds to the first snapshot.
## Validation and healing — the integrity taxonomy (settled 2026-07-29)
Every rule in this document that refuses, tolerates, recovers, or repairs is an instance of **one five-verdict taxonomy**, owned by one vocabulary — the **integrity service** (`IntegrityRules` + `HealScheduler`, 02-architecture.md ▸ Components). A detectable defect classifies into exactly one verdict, and the verdict fixes everything downstream — surface, write behavior, race posture — so no mechanism ever re-reasons its posture individually. The doctrine behind every lenient verdict (stated 2026-07-29): **leniency is recovering recoverable issues through reliable heuristics owned by this service — never accepting loss that could surprise the user.** Where a reliable recovery exists, it applies (silently where nothing is lost); where it doesn't, the defect surfaces — refusal and alerting are the honest ends of the same scale, and "proceed partially, lose a little" is never a verdict:
- **Refuse** — fail-fast (Malformed input above): the defect defeats rendering or ordering. Initial-load refusal; once open, the reload-breakage condition (02-architecture.md). **Refusal is the healer's last resort, and no longer a dead end** (re-ruled 2026-07-29 — fail-fast is *gated by the integrity service*): a defect with a reliable heal heals and moves on (a non-blocking notice at most); only an unhealable defect blocks, and blocking means a **decision surface** — the load pauses, the user chooses the fix, the fix applies, the load proceeds. Never a modal chain: the loader collects every fail-fast defect in the walk rather than stopping at the first, and one aggregated surface presents them all. The surface's design — per-defect-class decision vocabulary, aggregation shape, reconciliation with the welcome failure row, record-before-load, and the pre-snapshot loading state — is its own design pass (tracked on the findings board).
- **Refuse** — fail-fast (Malformed input above): the defect defeats rendering or ordering. Initial-load refusal; once open, the reload-breakage condition (02-architecture.md). **Refusal is the healer's last resort, and no longer a dead end** (re-ruled 2026-07-29 — fail-fast is *gated by the integrity service*): a defect with a reliable heal heals and moves on (a non-blocking notice at most); only an unhealable defect blocks, and blocking means a **decision surface** — the load pauses, the user chooses the fix, the fix applies, the load proceeds. Never a modal chain: the loader collects every fail-fast defect in the walk rather than stopping at the first, and one aggregated surface presents them all. The surface's design is settled (2026-07-31) — Malformed input above ▸ The decision surface.
- **Refuse writes** — readable-but-uneditable shapes (Frontmatter above): the file renders fine, and every app write to it fails loudly per file.
- **Tolerate** — outside the schema's claim: strays, symlinks, case-twins, lane- and board-level `deleted:`. Preserved verbatim, logged, never rendered (or rendered with the key inert).
- **Coerce** — a sensible reading exists (the coercion rulebook, last-wins, null-as-missing, the rescue family): silent, read-side only, bytes preserved — the recovered value is exactly what the writer meant.
+1 -1
View File
@@ -82,7 +82,7 @@ The non-modal banner named throughout the read- and write-side rules above is on
- **Restoration is a preference** — "Restore open boards at launch" in Settings (⌘, — 11-command-nexus.md), default on. On: boards open at last quit reopen (bookmark-resolved), with their per-board frames and 05's card-window restoration. Off: every launch starts at welcome.
- **The restoration set is a live open marker, never an at-quit write** (settled): each registry record (Per-board app state below) carries an open-now flag — set when the board's window opens and cleared on *user-initiated* close — quit's teardown closes deliberately leave it standing (the boards were open at quit by definition; teardown distinguishes user-close from quit-close, and that distinction is the whole mechanism). Restoration reads the flagged records, reopening by `lastOpened` order. Crash recovery falls out for free: after a crash the flags describe what was open at crash time, so relaunch restores it — no separate recovery logic, no once-at-quit stamp to race teardown or miss on a crash. The preference gates only whether the flagged set is consulted; the flags are maintained regardless.
- **Welcome appears only when nothing restores** — restoration off, nothing was open, or every restoration failed. Always reachable via Window ▸ Welcome to Lanework. Opening a board from welcome closes welcome.
- **Every open passes through a pre-snapshot loading state** (ruled 2026-07-29): the board window appears **immediately** — welcome click, File ▸ Open…, Finder double-click, restoration alike — at its saved frame, its chrome carrying the registry record's cached title and icon (the same no-scan sources the welcome row reads; a first-ever open shows the folder name, the record's provisional display name). The content area holds a quiet loading surface: a centered system spinner appearing only after a short grace (~200 ms) so ordinary fast opens never flash it — no skeleton lanes, the motion language animates real data only. The first snapshot replaces the surface in place (a snap — there is no prior arrangement to animate from). **The walk is cancellable**: ⌘W during loading cancels it and closes the window, an ordinary user-initiated close clearing the open-now flag. **Restoration is parallel**: every flagged window appears at once in loading state (stacked by `lastOpened` order), each walk independent — a slow network board never delays the others and stays closeable while it loads. Failure keeps its settled surface: a fail-fast walk retires its loading window and lands row-level on welcome — welcome returning if the open came from it or from Finder — never a loading window morphing into an error display. The open walk earns no in-progress info row: the loading state is the surface, and the info-row class stays scoped to copy-shaped work.
- **Every open passes through a pre-snapshot loading state** (ruled 2026-07-29): the board window appears **immediately** — welcome click, File ▸ Open…, Finder double-click, restoration alike — at its saved frame, its chrome carrying the registry record's cached title and icon (the same no-scan sources the welcome row reads; a first-ever open shows the folder name, the record's provisional display name). The content area holds a quiet loading surface: a centered system spinner appearing only after a short grace (~200 ms) so ordinary fast opens never flash it — no skeleton lanes, the motion language animates real data only. The first snapshot replaces the surface in place (a snap — there is no prior arrangement to animate from). **The walk is cancellable**: ⌘W during loading cancels it and closes the window, an ordinary user-initiated close clearing the open-now flag. **Restoration is parallel**: every flagged window appears at once in loading state (stacked by `lastOpened` order), each walk independent — a slow network board never delays the others and stays closeable while it loads. Failure resolves by attendance (amended 2026-07-31 — 01-storage-format.md ▸ The decision surface): an **attended** open's fail-fast walk transforms the loading content in place into the aggregated repair surface — a live decision, not an error display — and only Cancel retires the window to the row-level welcome landing; a **restored** window's walk keeps the settled behavior, retiring to welcome row-level — welcome returning if the open came from it or from Finder — and the row's retry click is the attended open that earns the surface. The open walk earns no in-progress info row: the loading state is the surface, and the info-row class stays scoped to copy-shaped work.
- **Closing the last board window leaves the app windowless** (menu bar alive) — the close is respected. Reactivation (Dock click) with no windows shows welcome.
- **A restored board that fails surfaces on welcome, row-level**: its loading window retires (the pre-snapshot state above); welcome appears alongside whatever did restore, the failed board's recents row carrying fail-fast's specifics (load error) or the unavailable state per Graceful orphaning (offline volume, dead bookmark). Other restorations proceed unaffected — never a launch-time modal chain, never a silent drop.
- **Opening a board from Finder is a standard document open** (settled): double-clicking a `.kanban` folder (or `open -a`) routes through the app's open-documents handler into the exact path welcome and File ▸ Open… already use — registry record (created before loading, Per-board app state below), board window, recents stamp. A board already open focuses its existing window — file-identity match, never a second window (one board window per root, above). A fail-fast failure surfaces row-level on welcome, uniform with the restored-board failure row.