Formalize the integrity taxonomy and service — the validator design pass
01 gains Validation and healing (five verdicts: refuse / refuse-writes / tolerate / coerce / heal; heal axes consent x timing incl. the inline mode; write discipline decided by the existing stamp rule; disk re-verify, memo discipline, defer-under-lock). kind re-ruled: any object may carry it, the value names the kind, trusted where consulted, no stripping; backfill is the service's on-touch heal, on-touch only. 02 gains IntegrityRules + HealScheduler components — rules and policy consolidate, enforcement stays with loader/writer. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
@@ -73,7 +73,7 @@ Body: board description (free Markdown).
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `order` | number | yes | Rank among lanes, ascending = left-to-right |
|
| `order` | number | yes | Rank among lanes, ascending = left-to-right |
|
||||||
| `width` | integer | no | Width multiplier ≥ 1 (default 1); no cap |
|
| `width` | integer | no | Width multiplier ≥ 1 (default 1); no cap |
|
||||||
| `kind` | string | no | Always `lane` (ruled 2026-07-29) — written at lane creation, healed on touch when absent, never stripped. Redundant with depth on the live board; load-bearing inside `.trash/`, where it discriminates a trashed lane from a card (Deletion below). Cards never carry it. |
|
| `kind` | string | no | Always `lane` (ruled 2026-07-29) — written at lane creation, backfilled on touch when absent (the integrity service's on-touch heal — Validation and healing below), never stripped. Redundant with depth on the live board; load-bearing inside `.trash/` (Deletion below). **Any object may carry `kind`, and the value — never the key's mere presence — names the object's kind** (re-ruled 2026-07-29): consumers that consult the key trust its value outright, with no stripping and no corroboration machinery; the live board never consults it (level is position), the trash consults it first. The app writes it only on lanes — accept liberally, emit conservatively. |
|
||||||
|
|
||||||
Body: lane description / WIP policy / notes.
|
Body: lane description / WIP policy / notes.
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ Carried over unchanged — gapped fractional ranks:
|
|||||||
The tombstone model is **retired** — no `deleted:` key, no in-place flags, no ancestor-walked hiding. Deletion is a **move**:
|
The tombstone model is **retired** — no `deleted:` key, no in-place flags, no ancestor-walked hiding. Deletion is a **move**:
|
||||||
|
|
||||||
- **Deleting a card or a lane moves its folder into `<board-root>/.trash/`** (lanes joined 2026-07-29 — retiring the design's sole destructive delete) — a reserved, app-claimed name at board root (joining `CLAUDE.md` and the seeded `.gitignore` on the claimed list; agents and hand-editors should treat it as the app's). `.trash/` holds card and lane folders interleaved directly, no `index.md` of its own. Every arrival lands at the **top** regardless of kind: the move mints an `order` rank above the current topmost, so the trash sorts newest-first by ordinary ranks — no timestamp sort, no tie-break rules. The move **stamps `modified`** (deletion is an edit to the item's story — the deliberate exception to moves-don't-stamp), which is what a future age-based auto-purge will read (deliberate follow-up, not in the initial release). A trashed lane keeps its subtree untouched — its cards ride along and come back with it.
|
- **Deleting a card or a lane moves its folder into `<board-root>/.trash/`** (lanes joined 2026-07-29 — retiring the design's sole destructive delete) — a reserved, app-claimed name at board root (joining `CLAUDE.md` and the seeded `.gitignore` on the claimed list; agents and hand-editors should treat it as the app's). `.trash/` holds card and lane folders interleaved directly, no `index.md` of its own. Every arrival lands at the **top** regardless of kind: the move mints an `order` rank above the current topmost, so the trash sorts newest-first by ordinary ranks — no timestamp sort, no tie-break rules. The move **stamps `modified`** (deletion is an edit to the item's story — the deliberate exception to moves-don't-stamp), which is what a future age-based auto-purge will read (deliberate follow-up, not in the initial release). A trashed lane keeps its subtree untouched — its cards ride along and come back with it.
|
||||||
- **`kind:` discriminates inside `.trash/`** (ruled 2026-07-29): depth defines meaning *on the live board*, but the trash is flat, and an empty lane folder is shape-identical to a card folder (`<uuid>/index.md` both). Lane `index.md` frontmatter therefore carries **`kind: lane` at all times** — written at lane creation, healed on touch when absent (minimum: the trash move stamps it; the fuller healing posture is the validation-service design, tracked on the Redesign board), never stripped (restore stays a pure move; the key is inert on the live board where depth wins). Cards never carry the key — absent means card. The trash reader's rule: `kind: lane` → lane; no key but UUID-shaped children with their own `index.md` → lane (self-heal the key); neither → card.
|
- **`kind:` discriminates inside `.trash/`** (ruled 2026-07-29): depth defines meaning *on the live board*, but the trash is flat, and an empty lane folder is shape-identical to a card folder (`<uuid>/index.md` both). Lane `index.md` frontmatter therefore carries **`kind: lane` at all times** — written at lane creation, backfilled on touch when absent (settled 2026-07-29 — the integrity service's on-touch heal, Validation and healing below: any Writer rewrite of that lane's `index.md`, the trash move's rank mint included, stamps the missing key; **on-touch only, never a scheduled backfill sweep**), never stripped (restore stays a pure move; the key is inert on the live board where depth wins). The trash reader's rule **trusts the value** (re-ruled 2026-07-29 — the value-names-the-kind posture, Frontmatter above): `kind: lane` → lane; `kind: card` → card; an unrecognized value or no key falls through to shape — UUID-shaped children with their own `index.md` → lane (the key backfills on the next touch), else card. Trusting the value means an external writer's `kind: lane` on what looks card-shaped is honored, never policed — the app never writes the key on cards, so its presence is a deliberate claim. Honest limit of on-touch-only backfill, accepted: a kind-less trashed lane emptied of its children before any touch becomes indistinguishable from a card.
|
||||||
- **Restore is a move out**: any move of a card folder from `.trash/` into a lane, or of a lane folder to board level — drag, or cut+paste — is an ordinary move; there is no restore-specific machinery and no Put Back. Position (and, for cards, lane) are chosen at restore time.
|
- **Restore is a move out**: any move of a card folder from `.trash/` into a lane, or of a lane folder to board level — drag, or cut+paste — is an ordinary move; there is no restore-specific machinery and no Put Back. Position (and, for cards, lane) are chosen at restore time.
|
||||||
- A folder that disappears entirely (Finder deletion) is also a delete; the app reflects it — such items are gone, they never enter the trash.
|
- A folder that disappears entirely (Finder deletion) is also a delete; the app reflects it — such items are gone, they never enter the trash.
|
||||||
- Physical removal in-app is the trash's **Delete / Delete Immediately / Empty Trash** (03-board-ui.md).
|
- Physical removal in-app is the trash's **Delete / Delete Immediately / Empty Trash** (03-board-ui.md).
|
||||||
@@ -127,6 +127,22 @@ Loud, specific error (path + what's wrong) for: unparseable YAML, missing requir
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
- **Refuse** — fail-fast (Malformed input above): the defect defeats rendering or ordering. Initial-load refusal; once open, the reload-breakage condition (02-architecture.md).
|
||||||
|
- **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.
|
||||||
|
- **Heal** — an app-owned invariant is violated *and* a lossless canonical repair exists: the only verdict that writes.
|
||||||
|
|
||||||
|
Heals classify on two axes. **Consent**: *app-initiated* when the repair is semantically unambiguous and content-lossless (loose-file relocation, legacy card migration, `kind` backfill, the agent guide); *user-gated* when the fix chooses identity or interpretation (the duplicate-id **Repair** mints identity, so it waits behind its condition banner — Fractal layout above). **Timing**, three modes: *inline* when the triggering gesture cannot proceed without it (the midpoint-exhaustion renumber, the import-boundary remint — each stays inside its gesture's write bracket and commit); *on-touch* when the defect is latent — the fix folds into a write already rewriting that file (`kind` backfill, duplicate-key twin removal, the colon rescue's quote-on-first-write); *scheduled* when the defect degrades behavior while it stands (loose files invisible to the attachment surface, `deleted:` cards rendering by a retired model, a stale agent guide actively misleading agents) — run at the reload tail and at board open, uniformly. The timing principle: **proactive when the defect is load-bearing now, on-touch when it's latent.**
|
||||||
|
|
||||||
|
**Write discipline needs no rule of its own** (settled — the existing stamp rule decides every case): a heal that rewrites an `index.md` is an ordinary app write — stamps `modified`, clears `modified-by`; an on-touch heal rides its host write's stamp — while a heal that only renames or relocates folders and files never opens `index.md` and stamps nothing (the relocation, the remint — identity repairs, not edits). Atomicity is per file (temp + rename); interrupted batches are accepted per the renumber precedent — every intermediate state is valid. **Every scheduled heal re-verifies its defect against disk at write time** and no-ops when it is gone: losing the race to a foreign fix — an agent mid-edit, another repair — is success, never an error (the Repair-races-a-vanished-duplicate precedent, generalized). Scheduled heals are memo-guarded against hot loops — armed before the attempt, cleared explicitly on success, re-armed only by a changed defect signature — and **defer, never abandon**, under any read-only lock.
|
||||||
|
|
||||||
|
The vocabulary is per-kind and extensible: adding an object kind (the enhanced schema's comment) adds its field table and shape rules in one place, never a parallel mechanism.
|
||||||
|
|
||||||
## Document packaging
|
## Document packaging
|
||||||
|
|
||||||
- Boards are created as `.kanban` packages (UTI conforming to `com.apple.package`) so Finder treats a board as one document.
|
- Boards are created as `.kanban` packages (UTI conforming to `com.apple.package`) so Finder treats a board as one document.
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ The **one named exception** is transient UI state rendering things that don't ex
|
|||||||
- **FolderWatcher** — FSEvents (debounced: **200 ms trailing**, the timer restarting per event so a burst yields one reload after quiet, over 50 ms FSEvents latency — settled numbers), attached best-effort to whatever path the board lives at. **Events under any `.git` path component are filtered out** (settled): the board's own root-level repo (a worktree-link `.git` file included) is the app's auto-commit churn, and a repo nested deeper — a card folder containing a clone, a submodule — is a stray (01-storage-format.md) whose internals never render; neither can alter the rendered tree, so neither drives reloads. (A nested repo's *working files* still fire events like any stray's — those reloads are value-equal and quiet.) There is only this one watching path: no NSMetadataQuery for iCloud Drive, no polling fallback for network volumes — on those warned-against locations (07-sync-collab.md) FSEvents delivery is unreliable and live reload silently degrades, accepted per 07's no-accommodations stance.
|
- **FolderWatcher** — FSEvents (debounced: **200 ms trailing**, the timer restarting per event so a burst yields one reload after quiet, over 50 ms FSEvents latency — settled numbers), attached best-effort to whatever path the board lives at. **Events under any `.git` path component are filtered out** (settled): the board's own root-level repo (a worktree-link `.git` file included) is the app's auto-commit churn, and a repo nested deeper — a card folder containing a clone, a submodule — is a stray (01-storage-format.md) whose internals never render; neither can alter the rendered tree, so neither drives reloads. (A nested repo's *working files* still fire events like any stray's — those reloads are value-equal and quiet.) There is only this one watching path: no NSMetadataQuery for iCloud Drive, no polling fallback for network volumes — on those warned-against locations (07-sync-collab.md) FSEvents delivery is unreliable and live reload silently degrades, accepted per 07's no-accommodations stance.
|
||||||
- **Ranks** — gapped fractional ordering math + compaction. Pure.
|
- **Ranks** — gapped fractional ordering math + compaction. Pure.
|
||||||
- **DropSlot** — drop-geometry math: hit zones and insertion-position targeting for drags (lane/position within the masonry, cross-board, Finder file drops). Pure, like Ranks.
|
- **DropSlot** — drop-geometry math: hit zones and insertion-position targeting for drags (lane/position within the masonry, cross-board, Finder file drops). Pure, like Ranks.
|
||||||
- **AgentGuide** — writes/upgrades the board-root `CLAUDE.md` (see 08-agent-integration.md).
|
- **AgentGuide** — writes/upgrades the board-root `CLAUDE.md` (see 08-agent-integration.md). Its refresh is a scheduled heal riding the HealScheduler (below).
|
||||||
|
- **IntegrityRules** — the one pure vocabulary of object validity (01-storage-format.md ▸ Validation and healing; settled 2026-07-29): the identity predicate and its canonical form (one rule shared by `ItemID` and the Writer's string-level checks — today's parallel `canonicalIdentity` derivation folds in), the per-field coercion rulebook, shape classification (the readable-but-uneditable shapes), per-kind index validation (the card validator generalized per kind — board, lane, card, the enhanced schema's comment when it lands), the reserved-name tables (card children, board-root claimed names — today scattered), the trash `kind` discriminator, and the typed **Defect** vocabulary the loader reports. `LoadResult`'s ad-hoc repair channels (loose files, legacy tombstones) become one typed defect stream; tolerate-tier warnings stay warnings — information, not work. **Loader and Writer remain the enforcement points and call in** — the service consolidates rules and policy, never relocates enforcement; a service smeared across the read/write/orchestration boundaries would be worse than the current discipline.
|
||||||
|
- **HealScheduler** — the scheduled-heal engine: the six-step pattern today re-derived per healer in BoardStore (loose-file relocation, tombstone migration, agent-guide refresh), expressed once — compute work from the latest defects → resting-clear when empty → lock-and-writability gate (the read-only-lock deferral plus the guide's narrow `isWritableFile` defense, generalized to every healer) → signature compare → arm the memo *before* attempting → one write bracket whose write half re-verifies each defect against disk → post per one banner-posture table (each defect class declares loss row / silent / failure-only once; BannerCenter still owns all phrasing) → **clear the memo explicitly on success** (today only the guide does; the others' resting states merely happen to converge). Fires uniformly at the reload tail and at registry acquire — closing today's asymmetry where tombstone migration never fires at open. Inline heals (the midpoint-exhaustion renumber-and-retry, the import-boundary remint) stay gesture-scoped, with the renumber's ask-renumber-ask-again two-step as one shared helper instead of today's nine hand-rolled copies; on-touch heals live at the Writer's `updateIndex` seam, which consults IntegrityRules for pending on-touch work on the file it is rewriting (`kind` backfill; the span editor's duplicate-key twin removal and quote-on-first-write are the same class, named).
|
||||||
- **HistoryStore** — the history provider behind the edition seam (12-editions.md): the board session binds one `HistoryProviding` implementation at composition — base Lanework's native undo stack (13-native-undo.md, inverse `WriteOperation`s over NSUndoManager) or Pro's git plumbing (06-history-undo.md). Pro-only source; the base target never links git machinery.
|
- **HistoryStore** — the history provider behind the edition seam (12-editions.md): the board session binds one `HistoryProviding` implementation at composition — base Lanework's native undo stack (13-native-undo.md, inverse `WriteOperation`s over NSUndoManager) or Pro's git plumbing (06-history-undo.md). Pro-only source; the base target never links git machinery.
|
||||||
|
|
||||||
### Live-reload resilience
|
### Live-reload resilience
|
||||||
|
|||||||
Reference in New Issue
Block a user