Claimed-name squatters heal by displacement — Lanework owns the board

User ruling on the squatted-.trash gap: a wrong-typed node on an app-claimed
name (a file or symlink squatting .trash, a directory or symlink squatting
CLAUDE.md) is an invalid artifact, not a resident — a scheduled heal moves
it aside via the Finder-style rename ladder, preserved verbatim, with the
relocation-style notice. Upgrades the agent guide's untouchable-skip to
displace-then-write; the CLAUDE.user.md-taken skip stands (destinations
don't cascade-displace). Until the heal lands the loader keeps the
empty-trash read and a racing delete fails as a one-shot — a one-reload
window, not a standing state.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-07-29 14:24:07 -04:00
parent bf559bbbb8
commit 62c47a2209
+3 -3
View File
@@ -31,7 +31,7 @@ MyBoard.kanban/ ← board = the document
- `index.md` = YAML frontmatter between `---` delimiters, then Markdown body.
- **Reserved child names** at card level: `attachments/` (specified below) and `comments/` (enhanced schema — see below). Never levels themselves, never identity (identity-bearing siblings are UUIDs).
- **Name shape gates level detection** (settled): only folders passing the identity predicate (the shape-only rule above — 8-4-4-4-12 hex, any case, any version) are lane/card candidates. Anything else — even with an `index.md` inside — is a stray: skipped with a log warning, preserved verbatim, never rendered. A hand-made `notes/` folder or a board nested inside another can never brick a load; the cost — a hand-authored lane named `todo/` doesn't render — is what the warning and the agent guide exist for.
- A UUID-named folder below the root **lacking `index.md` is skipped with a log warning**, not an error — an interrupted two-step create must not brick the board. **Stray tolerance is uniform at every level**: non-reserved files and folders at board, lane, or card level are ignored by the loader, preserved verbatim, never rendered (inside a card, only `index.md` and the reserved child names mean anything). **One carve-out — loose *files* in a card folder are relocated, not tolerated** (settled 2026-07-28, Lanework-owns-the-board): a regular file sitting beside a card's `index.md` (not `attachments/`, not a reserved name) belongs in `attachments/`, and the app moves it there — Finder-style rename on collision — surfacing a graceful warning-tone notice naming the card and files ("Moved 'notes.txt' into attachments — 'Fix login'"; plurals fold). Detection stays read-only in the loader (the Repair precedent); the relocation runs through the Writer as an app-mediated write, commits normally on git boards, and waits out any read-only lock (strays stay tolerated until it clears). The carve-out is exactly that narrow: stray *folders* in a card (a nested clone, a hand-made subfolder), symlinks (never touched, above), and everything at board or lane level keep the verbatim posture — relocating a directory into the flat attachment model would be wrong, and board/lane-level strays (`CLAUDE.user.md`, a `notes/` folder) are legitimate residents. The agent guide teaches the rule (08-agent-integration.md): card files belong in `attachments/`. **Three board-root names are app-claimed, not strays** — the one scope on the verbatim promise: `CLAUDE.md` is app-owned and rewritten on guide version bumps, with a markerless user-authored `CLAUDE.md` relocated to `CLAUDE.user.md` (08-agent-integration.md ▸ The agent guide — that name is claimed too), and adding git seeds `.gitignore` (06-history-undo.md ▸ Repository hygiene; seeded once, then the user's to edit). Agents and hand-editors should treat these names as the app's at board root; everything else is untouched.
- A UUID-named folder below the root **lacking `index.md` is skipped with a log warning**, not an error — an interrupted two-step create must not brick the board. **Stray tolerance is uniform at every level**: non-reserved files and folders at board, lane, or card level are ignored by the loader, preserved verbatim, never rendered (inside a card, only `index.md` and the reserved child names mean anything). **One carve-out — loose *files* in a card folder are relocated, not tolerated** (settled 2026-07-28, Lanework-owns-the-board): a regular file sitting beside a card's `index.md` (not `attachments/`, not a reserved name) belongs in `attachments/`, and the app moves it there — Finder-style rename on collision — surfacing a graceful warning-tone notice naming the card and files ("Moved 'notes.txt' into attachments — 'Fix login'"; plurals fold). Detection stays read-only in the loader (the Repair precedent); the relocation runs through the Writer as an app-mediated write, commits normally on git boards, and waits out any read-only lock (strays stay tolerated until it clears). The carve-out is exactly that narrow: stray *folders* in a card (a nested clone, a hand-made subfolder), symlinks (never touched, above), and everything at board or lane level keep the verbatim posture — relocating a directory into the flat attachment model would be wrong, and board/lane-level strays (`CLAUDE.user.md`, a `notes/` folder) are legitimate residents. The agent guide teaches the rule (08-agent-integration.md): card files belong in `attachments/`. **Three board-root names are app-claimed, not strays** — the one scope on the verbatim promise: `CLAUDE.md` is app-owned and rewritten on guide version bumps, with a markerless user-authored `CLAUDE.md` relocated to `CLAUDE.user.md` (08-agent-integration.md ▸ The agent guide — that name is claimed too), and adding git seeds `.gitignore` (06-history-undo.md ▸ Repository hygiene; seeded once, then the user's to edit). Agents and hand-editors should treat these names as the app's at board root; everything else is untouched. **A claimed name held by the wrong kind of node is Lanework's to heal — by displacement** (ruled 2026-07-29 — Lanework owns the board, so an invalid artifact on a claimed name is a defect, not a resident): a regular file or symlink squatting `.trash` (a directory name), or a directory or symlink squatting `CLAUDE.md` (a file name), is moved aside by a scheduled heal (Validation and healing below) via the Finder-style rename ladder (`.trash``.trash 2`), **preserved verbatim, never destroyed**, with the relocation-style warning-tone notice naming old and new; the freed name then serves the app (the next delete mints the real `.trash/`; the guide write proceeds). This upgrades the agent guide's former untouchable-skip to a displacement — the invariant that survives is displacement-never-destruction. One standing exception: the settled `CLAUDE.user.md`-taken skip stands — that displacement has a designated *destination*, and freeing a destination by a second displacement would cascade renames; the guide write skips with a log as settled (08-agent-integration.md).
- **Symlinks are never traversed** (settled): the loader and the watcher treat any symbolic link in the board tree — identity-shaped name or not — as a stray: skipped with a log warning, preserved verbatim, never rendered, never followed. A link passing the identity predicate would otherwise render bytes living outside the board that FSEvents never reports — the silently-stale board 02-architecture.md structurally excludes — and a link cycle would make the tree walk non-terminating. Copy flows copy the link itself, never its target: Duplicate, Save as Template, instantiation, cross-board copies, and clipboard staging preserve the link verbatim as they do any stray.
- **Atomic writes**: temp file, rename over `index.md`.
- **Unknown frontmatter keys and their order are preserved verbatim** on every rewrite. This is load-bearing for agent overlays (`project:`, `sphere:`, custom tags) — see 08-agent-integration.md.
@@ -114,7 +114,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**:
- **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. A non-directory node squatting the name — a file, a symlink — is an invalid artifact healed by displacement (the claimed-names rule above): until the heal lands, the loader reads the board as having an empty trash and a delete's directory mint fails as an ordinary one-shot — a window measured in one reload, not a standing state. 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). Every object carries `kind` from creation (re-ruled 2026-07-29 — the common-schema row, Frontmatter above); inside `.trash/` it is where the key earns its keep. A lane's `kind: lane` is **backfilled on touch when absent** (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**) and never stripped (restore stays a pure move; the key is inert on the live board where position wins). The trash reader's rule **trusts the value** (re-ruled 2026-07-29 — the value-names-the-kind posture): `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. 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.
- A folder that disappears entirely (Finder deletion) is also a delete; the app reflects it — such items are gone, they never enter the trash.
@@ -137,7 +137,7 @@ Every rule in this document that refuses, tolerates, recovers, or repairs is an
- **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.**
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, a wrong-typed node squatting a claimed name — deletion is broken while it stands) — 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.