diff --git a/DESIGN/01-storage-format.md b/DESIGN/01-storage-format.md index 66ec1db..d52abe3 100644 --- a/DESIGN/01-storage-format.md +++ b/DESIGN/01-storage-format.md @@ -85,7 +85,7 @@ Body: lane description / WIP policy / notes. Body: the card's content — the whole point. -Schema-owned display fields are Lanework's to interpret — **coerce where a sensible reading exists, fall back to the field's default where none does** (settled). A scalar of the wrong YAML type reads as its source text (`title: 2048` displays as "2048", `width: "2"` reads as 2); where no sensible reading exists — a sequence or mapping where a scalar belongs, a non-integer width, an unparseable timestamp — the field falls back to its default: untitled placeholder, width 1, no color, no icon. **A no-sensible-reading fallback logs** (ruled 2026-07-29 — the family posture: every silent recovery leaves a trace): field, path, and raw text, carried as coerce-tier entries in the integrity service's Defect stream (Validation and healing below) — the one place where an observed-in-the-wild shape can later be promoted to a heuristic heal or a notice; no banner, no behavior change. **Ranges are part of the sensible reading** (settled): an integer `width` below 1 reads as 1 — the table's ≥ 1 is a validity bound, and 03-board-ui.md's width division must never see a zero or negative unit — while a non-finite `order` (`.nan`, `.inf`) is the same loud malformed-input rejection as a non-numeric one: fail-fast's `order` guard covers unusable values, not merely non-numeric ones (NaN has no place in the total order the tie-break and midpoint math assume). The hostile-YAML suite gains both shapes (`width: 0`, `order: .nan`). Coercion is read-side only; the bytes on disk are **preserved verbatim, never rewritten**. One legacy nuance: a *present* `deleted:` key is migration input, not live state — the item relocates per the Deletion section's legacy rule, whatever the timestamp's condition. **Duplicate keys: last one wins** (settled — the coercing read; strict YAML would reject the file, so this is a deliberate divergence in the editor's favor): a *top-level* key appearing twice reads as its last occurrence, earlier occurrences preserved verbatim on disk and invisible. A duplicate inside a nested mapping value remains a YAML error — the rescue applies where the editor's slip actually happens, the top level. Last-wins resolves *before* strictness: a duplicated `order` reads as its final occurrence and only that value faces the malformed-order check — duplication itself is never a fail-fast. An app write of a duplicated key rewrites the winning (last) occurrence and removes the earlier ones — the app owns the keys it writes, and leaving a stale twin would resurrect it if the winner were later removed; removing a key removes all its occurrences. **App rewrites preserve comments** (settled): comments on their own lines always survive a rewrite; an inline comment on a rewritten value line is re-spliced after the new value — best-effort, guaranteed for plain scalar lines (the realistic case), dropped only in pathological shapes. **Unquoted colons in a value read as the string they meant** (ratified — the third member of the rescue family, same posture as last-wins and coercion): the classic slip `title: Fix: the thing` — invalid YAML ("mapping values are not allowed in this context") — recovers when, and only when, the scanner error's mark lands on a line that *starts a top-level key* and whose remainder is a **plain unquoted scalar**: everything after the first `: ` reads as the value. A remainder opening with a quote or flow character (`"`, `'`, `[`, `{`) is a different breakage and stays fail-fast; nested/indented shapes never match the guard and stay errors. Mechanism mirrors the duplicate-key rescue: compose retries on a modified copy (the offending value swapped for a same-line placeholder), so one recovered line leaves every other line's diagnostics intact — a second, genuinely broken line still fails loudly with its own error. Read-side only, bytes preserved verbatim; the span editor keys by the line's key prefix, so the file stays fully editable, and the first app write of that key emits the value properly quoted — the slip heals on first touch. Recovery is **silent with a log line** (the family's posture: the recovered value is exactly what the writer meant, so there is nothing actionable to surface). The agent guide keeps teaching quoting — the rescue is a net, not a license. Test obligation: the hostile-YAML suite gains the `key: value: more` shape (none covers it today). Fail-fast remains reserved for structure (`schema`, `order`, YAML validity) — and it covers *malformed*, not just missing: an `order` that is present but non-numeric is the same loud malformed-input rejection as a missing one. **Readable-but-uneditable shapes load; writes to them refuse** (settled): frontmatter the surgical editor can't key by spans — a whole-frontmatter flow mapping, non-scalar keys, or frontmatter whose YAML resolves to an *empty* mapping while still carrying text (`{}`, `null`, `~` between the delimiters: zero keys load fine, but appending a key after that text would produce unparseable YAML; truly blank or comment-only frontmatter stays editable) — reads and renders normally, and any app write to that file fails loudly through the per-file write-failure banner (02-architecture.md ▸ Write-failure surfacing) naming the shape, never a silent corruption and never a load rejection. **Compound operations are transactions against this rule** (ruled 2026-07-29): every copy flow that rewrites descendants' `index.md` (paste, ⌥-drag duplicate, the cross-board copy, template instantiation — the stamp and the `modified-by` clear are part of the copy contract, not optional bookkeeping) **preflights the entire subtree** and refuses whole, loudly, naming the offending item — never a partial copy, never a silently unstamped descendant; the finest-grain precedent covers identity collisions, not skipped contract work. A mid-flight failure after a clean preflight (disk error) removes the partial result wholesale — construct-then-clean (09-templates.md's atomicity). Whole-board forks (Duplicate, Save as Template) stamp nothing and are exempt. +Schema-owned display fields are Lanework's to interpret — **coerce where a sensible reading exists, fall back to the field's default where none does** (settled). A scalar of the wrong YAML type reads as its source text (`title: 2048` displays as "2048", `width: "2"` reads as 2); where no sensible reading exists — a sequence or mapping where a scalar belongs, a non-integer width, an unparseable timestamp — the field falls back to its default: untitled placeholder, width 1, no color, no icon. **A no-sensible-reading fallback logs** (ruled 2026-07-29 — the family posture: every silent recovery leaves a trace): field, path, and raw text, carried as coerce-tier entries in the integrity service's Defect stream (Validation and healing below) — the one place where an observed-in-the-wild shape can later be promoted to a heuristic heal or a notice; no banner, no behavior change. **The unified log is the stream's consumer** (ruled 2026-07-30): the structured log line — subsystem-filterable in Console.app — is the whole surface this version, and promotion is deliberately manual, investigation-driven: a reported oddity leads to the log, the log names the shape, the shape informs the next heuristic. A no-telemetry app has no other observer to build for; an in-app diagnostics surface listing what a load coerced or tolerated is WISHLIST #11, and the typed stream is the plumbing it would read. **Ranges are part of the sensible reading** (settled): an integer `width` below 1 reads as 1 — the table's ≥ 1 is a validity bound, and 03-board-ui.md's width division must never see a zero or negative unit — while a non-finite `order` (`.nan`, `.inf`) is the same loud malformed-input rejection as a non-numeric one: fail-fast's `order` guard covers unusable values, not merely non-numeric ones (NaN has no place in the total order the tie-break and midpoint math assume). The hostile-YAML suite gains both shapes (`width: 0`, `order: .nan`). Coercion is read-side only; the bytes on disk are **preserved verbatim, never rewritten**. One legacy nuance: a *present* `deleted:` key is migration input, not live state — the item relocates per the Deletion section's legacy rule, whatever the timestamp's condition. **Duplicate keys: last one wins** (settled — the coercing read; strict YAML would reject the file, so this is a deliberate divergence in the editor's favor): a *top-level* key appearing twice reads as its last occurrence, earlier occurrences preserved verbatim on disk and invisible. A duplicate inside a nested mapping value remains a YAML error — the rescue applies where the editor's slip actually happens, the top level. Last-wins resolves *before* strictness: a duplicated `order` reads as its final occurrence and only that value faces the malformed-order check — duplication itself is never a fail-fast. An app write of a duplicated key rewrites the winning (last) occurrence and removes the earlier ones — the app owns the keys it writes, and leaving a stale twin would resurrect it if the winner were later removed; removing a key removes all its occurrences. **App rewrites preserve comments** (settled): comments on their own lines always survive a rewrite; an inline comment on a rewritten value line is re-spliced after the new value — best-effort, guaranteed for plain scalar lines (the realistic case), dropped only in pathological shapes. **Unquoted colons in a value read as the string they meant** (ratified — the third member of the rescue family, same posture as last-wins and coercion): the classic slip `title: Fix: the thing` — invalid YAML ("mapping values are not allowed in this context") — recovers when, and only when, the scanner error's mark lands on a line that *starts a top-level key* and whose remainder is a **plain unquoted scalar**: everything after the first `: ` reads as the value. A remainder opening with a quote or flow character (`"`, `'`, `[`, `{`) is a different breakage and stays fail-fast; nested/indented shapes never match the guard and stay errors. Mechanism mirrors the duplicate-key rescue: compose retries on a modified copy (the offending value swapped for a same-line placeholder), so one recovered line leaves every other line's diagnostics intact — a second, genuinely broken line still fails loudly with its own error. Read-side only, bytes preserved verbatim; the span editor keys by the line's key prefix, so the file stays fully editable, and the first app write of that key emits the value properly quoted — the slip heals on first touch. Recovery is **silent with a log line** (the family's posture: the recovered value is exactly what the writer meant, so there is nothing actionable to surface). The agent guide keeps teaching quoting — the rescue is a net, not a license. Test obligation: the hostile-YAML suite gains the `key: value: more` shape (none covers it today). Fail-fast remains reserved for structure (`schema`, `order`, YAML validity) — and it covers *malformed*, not just missing: an `order` that is present but non-numeric is the same loud malformed-input rejection as a missing one. **Readable-but-uneditable shapes load; writes to them refuse** (settled): frontmatter the surgical editor can't key by spans — a whole-frontmatter flow mapping, non-scalar keys, or frontmatter whose YAML resolves to an *empty* mapping while still carrying text (`{}`, `null`, `~` between the delimiters: zero keys load fine, but appending a key after that text would produce unparseable YAML; truly blank or comment-only frontmatter stays editable) — reads and renders normally, and any app write to that file fails loudly through the per-file write-failure banner (02-architecture.md ▸ Write-failure surfacing) naming the shape, never a silent corruption and never a load rejection. **Compound operations are transactions against this rule** (ruled 2026-07-29): every copy flow that rewrites descendants' `index.md` (paste, ⌥-drag duplicate, the cross-board copy, template instantiation — the stamp and the `modified-by` clear are part of the copy contract, not optional bookkeeping) **preflights the entire subtree** and refuses whole, loudly, naming the offending item — never a partial copy, never a silently unstamped descendant; the finest-grain precedent covers identity collisions, not skipped contract work. A mid-flight failure after a clean preflight (disk error) removes the partial result wholesale — construct-then-clean (09-templates.md's atomicity). Whole-board forks (Duplicate, Save as Template) stamp nothing and are exempt. ## Enhanced schema (reserved; comments storage specified 2026-07-29, feature out of scope) diff --git a/WISHLIST.md b/WISHLIST.md index 16c187a..9861df4 100644 --- a/WISHLIST.md +++ b/WISHLIST.md @@ -43,3 +43,7 @@ Comments are window-scoped by design (DESIGN/01 ▸ Enhanced schema — the boar ### 10. Search scope options Board search covers all meaningful card content (DESIGN/04 ▸ Search, re-ruled 2026-07-29 — title + body, comment bodies when comments ship, attributes as they activate). An either/or scope control — content vs attributes, or per-field filters — was anticipated in the same ruling but not designed. Decide when a real query pattern demands narrowing; the transient search index is the natural host for field-aware matching. + +### 11. Board diagnostics surface (coerce/tolerate report) + +The loader's Defect stream carries coerce-tier fallbacks (field, path, raw text) and tolerate-tier warnings, consumed this version by the unified log alone (DESIGN/01 ▸ coercion, ruled 2026-07-30 — promotion to heuristics is manual, investigation-driven). An in-app surface — a popover line or a per-board report window listing what the last load coerced, tolerated, and healed — would serve the agents-edit-boards workflow without Console.app. The typed stream is already the right plumbing; design open: where it lives, when it refreshes, and how it words defect classes without inventing a second banner vocabulary.