Commit Graph
302 Commits
Author SHA1 Message Date
rzen 9ca8ed84de 08's guide description goes present-tense — per-version changelog retired
2026-07-30 findings-resolution ruling (Redesign card c70d7a72): the
guide-contents section now states what the shipped guide teaches NOW,
normative against the literal, edited in the same change as the literal.
The hand-maintained per-version bullets drifted within days of v6
(claiming kind and lane-trash teachings the shipped body lacked) — the
README deferred-list failure class; version history lives in git.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-30 15:12:22 -04:00
rzen 0bec9a6be5 Collapse the edition split — one app, Pro as a subscription
2026-07-30 design pass (with the user), superseding the 2026-07-27
two-app split and the 2026-07-29 App Group ruling. Rulings:

- One Mac App Store app (dev.rzen.indie.Kanban, free, 2.0 updates the
  existing record); Lanework Pro = auto-renewable subscription (StoreKit
  2) unlocking the git tier; libgit2 compiles in dormant. The KanbanPro
  target, module-alias tests, verify-editions, UTI/About/Types twins all
  retire. Teams stays deferred — probably a separate app, and never on a
  shared app group.
- No grandfathering: 1.x's free git users get the inert-.git posture;
  unsubscribed and lapsed are one state — histories freeze, resume on
  subscribe via the committer's catch-up commit.
- Entitlement = local read of StoreKit's signed transaction store at
  board-session composition; never a network call on the open path;
  subscribe effective at next open (purchase flow offers reopen); lapse
  never interrupts an open session; expiry-while-offline with auto-renew
  on holds in grace until a real refresh.
- App Group removed wholesale: registry, clipboard staging, template
  store, defaults return to ordinary Application Support; grant slots
  collapse to one bookmark, open-now to one flag; freshness stamp,
  pid-liveness line, group-id provisioning risk all dissolve.
- Quiet signposts carry over; Settings gains the Pro section (subscribe/
  manage/restore) as the third named Pro surface.

12-editions.md rewritten as the tiers doc; edition vocabulary swept to
tiers across 00-13, README, WISHLIST.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-30 15:04:09 -04:00
rzen c741b02016 The unified log is the coerce stream's consumer — promotion is manual
2026-07-30 findings-resolution ruling (Redesign card 8fec5d7d): the
structured log line is the coerce-tier Defect stream's whole surface
this version; promotion to a heuristic heal or notice is deliberately
investigation-driven — a no-telemetry app has no other observer to
build for. An in-app diagnostics surface becomes WISHLIST #11 with the
typed stream as its ready plumbing. Code already conforms.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-30 14:43:54 -04:00
rzen 2ec2c95a63 Ratify the registry freshness stamp — two editions, one file, stat-checked
2026-07-30 findings-resolution ruling (Redesign card 89862cef): the
mtime+size stamp the App Group implementation introduced (566deab) is
the designed mechanism, not an invention — checked before every registry
read and mutation, re-read on change, write on top of the other
edition's latest state. The microsecond check-to-write window stays
last-writer-wins, costing at most one convenience field. Deliberately no
NSFileCoordinator and no per-record split. Code already conforms.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-30 07:20:43 -04:00
rzen ae1dd96af6 Delete Immediately is removed — permanence is only reachable inside the trash
2026-07-30 findings-resolution ruling (Redesign card d40bfac1): Finder's
⌥⌘⌫ answered disk-space pressure boards don't have, and it was the one
gesture reaching unrecoverable straight from the board. The delete
vocabulary is now purely staged: board → trash, trash → permanent
(confirmed on no-git boards), Empty Trash for bulk. Docs drop the File
menu row, both ⌥-alternate context rows, and the VO custom action; code
removal rides the session's realign card.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-30 07:17:11 -04:00
rzen 69084fdff7 Realign code with the 2026-07-29 findings-resolution rulings
Nine rulings land as code. Reorders don't stamp — one container-change
predicate (WriteOperation.rewritesOrderOnly): within-container reorders
and the renumber rescale rewrite only order, while cross-lane, cross-board,
and trash moves stamp modified and clear modified-by; no trash special
case exists, and the m8 undo inverses conform through the same seam.
Copies are transactions: the root-strict/nested-lenient split retires for
a whole-subtree stampability preflight that refuses loudly naming the
offender, and every item-level copy severs remote/remote-state at every
level (whole-board forks carry them verbatim). Paste refuses, never
degrades: the embedded-index.md materialization and its loss row retire;
a missing staged snapshot produces nothing and posts an error-tone
one-shot named from manifest metadata. Coerce-tier fallbacks log through
the Defect stream with path context attached loader-side. Displacement is
level-uniform: a file squatting attachments inside a card heals by the
same rename ladder as board-root squatters; comments stays tolerated.
Delete Immediately joins card and lane context menus as Delete's
⌥-alternate with its own VO custom action, routed through an explicit
container so the menu target outranks standing selection. Agent guide v7
teaches the stamp discipline and the card-level attachments claim, and
sheds two stale v6 lines (lanes trash now; kind is taught). Verified
conformant, unchanged: edition-aware Undo/Redo disable, trash marquee
full-height backdrop.

Both schemes 1854 tests / 318 suites green; verify-editions 30/30.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-30 06:49:11 -04:00
rzen 5ae48de0ea Reorders don't stamp; container changes do — the moves-don't-stamp rule refined
User-ruled 2026-07-30: only within-container order-only rewrites (a reorder
among a lane's siblings, a lane reorder on the board, the renumber rescale)
skip the modified stamp and the modified-by clear. A move that changes the
item's container — cross-lane, cross-board, into or out of .trash/ — stamps
and clears both: which lane a card lives in is state, where it stands in
line is presentation. The trash move stops being an exception and becomes
the container rule's plainest instance. 01 scope/modified-by/Ordering/
Deletion/comments, 03 Trash, and 08's v7 bullet updated.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-30 02:15:38 -04:00
rzen 90954a47d6 Disown AppKit window restoration — launches present welcome again instead of a windowless shell
macOS keeps Saved Application State per app, and on macOS 26 its mere
existence — even describing zero windows, which repeated dev kills
guarantee — counts as "a restored session": SwiftUI then treats every
scene's defaultLaunchBehavior as moot and presents nothing. The app
launched as a windowless shell with no way back, since windowOpener is
captured by the first scene that appears — so Open Recent, the
re-grant Grant click, and Dock reopen all silently buffered or
no-opped. Proven by -ApplePersistenceIgnoreState YES presenting
correctly on the same state; with the fix, welcome presented 3/3
consecutive plain launches.

Three changes:
- App.init registers ApplePersistenceIgnoreState — restoration is the
  registry's job (02 § Launch and window lifecycle), every scene
  already declares restorationBehavior(.disabled), and AppKit's layer
  was pure liability. Registered before NSApplicationMain runs, which
  is what makes a registration-domain default early enough.
- The restore bootstrap presents at every launch as the app's one
  reliable presenter; welcome is never system-presented (.suppressed)
  — the pass opens it when nothing else lands on screen.
  LaunchPlan.presentsBootstrap retired.
- captureWindowActions returns the replayed Finder-open count so the
  pass counts those as opens — a cold document launch doesn't get
  welcome stacked beside its board.

Both suites green, verify-editions 30/30.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 22:48:04 -04:00
rzen 546ed94412 The pasteboard UTType constants become edition twins — Pro stops claiming ownership it doesn't have
Latent since the m7 split: base's Info.plist exports the three family
pasteboard types (cards, lanes, clipboard) and Pro's imports them, but
the shared constants declared all three UTType(exportedAs:) — a claim
of ownership the system checks against the running app's plist. In Pro
that claim is false, and the first touch (the clipboard type, via the
launch sweep) raised a runtime fault that blocked board loading under
the debugger.

EditionAbout's twin-file pattern, applied to an initializer: the
constants move to Kanban/App/EditionTypes.swift (exportedAs) with a
KanbanPro/Edition/EditionTypes.swift twin (importedAs, identifiers
verbatim); project.yml excludes base's copy from Pro. Type identity is
the string, so payloads cross editions unchanged. Verified: Pro
fixture launch opens the board with zero UTI warnings; both suites
green; verify-editions 30/30.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 21:46:27 -04:00
rzen 27158a06cd Resolve the second-pass review findings — comments undo via .trash recursion, search reach, refuse-don't-degrade paste
Thirteen findings cleared with the user (Redesign board: 3 Contradictions + 3 Gaps + 7 Minors → Resolved):

- User template store re-homes to the shared App Group container (09/02) — templates cross editions; App Group implementation card annotated mid-flight.
- Comment delete = move into comments/.trash/ (01/05/13) — the materialized-trash pattern one level down; no byte capture returns on any edition; purge at window close, copies strip it; 13's absolutes stay true.
- Find & search: card-window ⌘F covers the focused comments pane; board search re-ruled to all meaningful card content — comment bodies via a search-owned transient index when comments ship, attributes as they activate (04/01/05); scope options = WISHLIST #10.
- Comments pane visibility = one persisted app-wide bit, à la Show Trash — content-derived auto-show retired (05/11).
- Comment attachments author in-window: composer/inline-edit drop carve-outs + paperclip (05).
- Item-level copies sever remote/remote-state at every level; whole-board forks exempt (01).
- Paste refuses loudly, never degrades — degraded embedded-index.md fallback and its loss row retired; Finder's whole-or-nothing invariant adopted (04/12/02).
- Escape: composer moves focus out (never discards a draft); inline comment edit = Cancel (05/11).
- Minors: 05 three-pane changes bullet, 01 comments-UI parenthetical, 07 TOFU store naming, WISHLIST item 3 trash wording, README deferred-list now points at the findings board.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 21:46:01 -04:00
rzen bebbc877db The held window chrome survives detach — toolbars and the board-info widget render again
No board window has shown its NSToolbar or the titlebar board-info
chevron since m6: on macOS 26, SwiftUI stages a scene's content
against a provisional window, then dismantles and re-makes the
window-sensing background view while moving content into the real
window. HostedWindowController.detach() treated that dismantle as the
window's death and discarded the held titlebar accessory and toolbar
controller — so the real window attached moments later to empty slots.
Observed live via instrumented launch: install arrives with no window,
detach follows (slots cleared), then the real window attaches.

detach() now removes the chrome from the window but retains the slots
— their lifetime is the controller's, not the sensing view's — and
attach() reinstalls whatever is held. Verified on a fixture launch:
the board window carries its toolbar and the leading accessory.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 21:32:20 -04:00
rzen 566deab506 Home app-side state in the shared App Group container
Every edition declares group.dev.rzen.indie.Kanban and homes its
app-side state there from day one (12-editions.md ruling 2026-07-29):

- AppGroup namespace: container resolution with per-edition fallback
  when unprovisioned, shared UserDefaults suite, edition identity, and
  a unit-test-host redirect (the test host IS the app — its launch
  sweep and recents refresh must not touch the real shared container).
- BoardRecord: bookmark/isOpenNow replaced by per-edition grants and
  openNow keyed by bundle id; hand-written Codable keeps legacy keys
  decoding (adopted in memory as the running edition's slots, upgraded
  on first save); every other field stays common.
- RecentBoard gains needsReopen: no grant of ours but somebody's —
  first click runs an open panel pre-anchored at the recorded path,
  prompt "Grant"; recordOpen mints this edition's slot onto the
  matched shared record (path fallback only after identity fails and
  only against records holding no grant of ours, so re-granting never
  forks the record).
- Cross-edition freshness: stat-cheap mtime+size stamp re-reads the
  registry when the sibling edition wrote it, so one edition's save
  never erases the other's records wholesale.
- restorables() filters on this edition's open-now flags; the board
  popover gains BoardEditionPresence ("Also open in Lanework Pro"),
  pid-liveness-checked so crash residue never lies.
- Clipboard staging store moves to the group container; the sweep
  claims doomed trees by atomic rename into .sweeping/ then deletes,
  so the sibling's concurrent sweep is a non-event.
- Template store re-homed to the group container per the 09-templates
  re-ruling; scalars (quick-style recents, window size) move to the
  shared suite.
- verify-editions.sh: 30 checks (each edition carries exactly the
  family group). No pathfinder 1.x migrator: 1.x predates the
  registry; state starts fresh in the group container.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 20:18:15 -04:00
rzen a99e1a52f0 Comments card-window UI designed — three componentized panes and the .draft composer
Designed with the user 2026-07-29, closing Redesign card 82e2de6c (both halves). The card window recomposes as three componentized panes — body, comments, attributes — with a layout option (side-by-side default, body-over-comments for narrow displays; app-wide) and the thread visible through body Edit in both mounts. The column auto-shows when the thread is non-empty; sort direction flips at the header; the composer sits at the newest end. The composer edits comments/.draft/ — the user's idea, replacing any app-side draft store: one draft per card, ordinary comment schema with attachments, excluded from the thread, riding git/copies/trash, joining the claimed names; slow save cadence composing "Draft comment on 'X'"; posting renames to a fresh UUID and restamps created/modified in one bracket. Inline edits are body-edit sessions in miniature; no prompts anywhere. All editions ship comments — only tracker sync is Teams (matrix updated). A11y shapes in 10; menu, grammar, context-menu, and configuration rows in 11.

Board: Redesign card 82e2de6c → Resolved; Implementation card "Build comments" filed on Backlog (post-2.0).

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 18:48:08 -04:00
rzen 0463540aea Dedupe duplicate ids and heal them silently
The crash-class gap the integrity design pass found (DESIGN/01 -
Fractal layout rules; 02 - Live-reload resilience): the loader had no
board-wide dedupe at all, so two hand-copied folders sharing a UUID put
two equal ItemIDs into one snapshot - which SwiftUI's ForEach does not
tolerate. Built to the day's re-rulings, both landing mid-flight: the
user-gated Repair banner retired (176c852 - the heal runs silently) and
the container boundary became the first tie-break (f153e79 - the
visible card never loses to its own trash ghost).

IntegrityRules.dedupe (pure, occurrence list in, verdict out): group by
canonical identity, collapse case-spelled twins first - spellings with
a live occurrence outrank trash-only spellings, then canonical
lowercase, then lexicographically first; losers are silent strays
(LoadWarning.caseTwinIgnored - spelling artifacts, never reminted) -
then earlier-occurrence-wins across the surviving spelling's folders on
a four-rung ladder: live-before-trashed, git path history rank, FS
birth date (nil is no comparison, never .distantPast), traversal
order. Occurrences are exactly the identity-bearing folders: lanes,
cards, .trash entries - a UUID-shaped folder under a card is content.

BoardLoader walks lanes as WalkedLane and builds Lane values only on
the far side of the verdict, so a withheld card can never reach a
snapshot; a name-only gate keeps the healthy-board cost at one
dictionary pass, no disk reads. Withheld subtrees are still walked - a
hand-copied lane's nested cards are their own withheld occurrences,
reminted at the finest grain like the import boundary would have. A
withheld trash entry's trashKinds reading leaves with it. The git rung
is a seam (BoardLoader.IdentityHistoryRanker, one closure keyed by
root-relative path) because base links no git machinery - base injects
nothing and falls through; pro-m1 owns the ranker (card annotated).

The heal: Defect.duplicateIdentity (signature duplicate:<path>:<id>)
rides HealScheduler as the fourth scheduled heal, ordered last among
the content heals because a remint renames folders and would stale the
paths the same load handed the relocation and migration.
BoardWriter.remintDuplicateIdentity re-verifies twice at write time -
the folder still carries the losing identity AND something else still
does (the vanished-duplicate race no-ops from either side) - then
renames to a fresh v4 minted against the whole board's identity bag.
A rename and nothing else: no index.md opened, no modified stamp, no
modified-by clear; the receipt is heal-marked (pro-m1's committer
splits it out, named by 06's kept Repair verb); no undo step - heals
are not gestures. The notice is the design's own sentence ("Repaired
duplicate id - 'Fix login'"; several fold to a count), a loss row on
the relocation's reasoning; WriteOperation.repairDuplicateID carries
the failure mirror.

Fixture repair rode along: duplicate-order-tie-break.kanban had a lane
and its own card sharing a UUID - a genuine duplicate the new pass
correctly withholds; the folder rename landed in feae6d0, the matching
test constant lands here.

66 tests added (DuplicateIdentityTests: the ladder rung by rung, the
straddles, withheld-lane subtrees, remint idempotence and races, the
one-heal-cycle window, all phrasing). 1804 green on both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 18:19:52 -04:00
rzen 68fa503250 Comments storage schema specified — fractal folders, chronological, window-scoped
Designed with the user 2026-07-29 (Redesign card 82e2de6c, storage half; UI half remains open). A comment is a UUID folder under comments/ holding index.md + optional attachments/ — a card's anatomy one level down, every fractal rule applying verbatim. Field table: kind: comment, lenient self-reported author (content, not overlay — survives app writes), created/modified; no title, no order. Threads sort chronologically by created (ranks rejected: independent clocks across machines and trackers), flat this iteration with nesting deferred. Comment defects never refuse the board — stray posture at worst. Delete skips the trash, undoable without confirm. Copies carry the thread, reminted, stripping remote/remote-state so two local comments never claim one remote object. Comments are window-scoped, outside the board snapshot — the walk stays O(cards); the committer already protects them via whole-root staging, and composer/announcer describe foreign comment changes by path shape. Count chip on faces filed as WISHLIST #9. Schema edition-blind.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 18:17:51 -04:00
rzen f153e79156 The dedupe universe includes .trash/ — and the container boundary is the first tie-break
Stated 2026-07-29: when duplicate occurrences straddle live and trashed, the live one keeps the identity regardless of age — a restore done as a copy must never see the restored card withheld in favor of its trash ghost; the silent heal remints the trashed occurrence. Uniform across content duplicates, case-twins, and trashed lanes sharing a live lane's UUID.

Board: Minor card 94086f7b → Resolved. Minor lane now holds only tombstoned sweep cards.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 18:00:58 -04:00
rzen 61c31c0c5d Restore the trash marquee full-height clause the rewrites dropped
The below-last-row rubber-band ruling (Resolved card 4ff6537d) survived two trash-section rewrites in code but not in prose; 04's origin clause now states the extent — full column height, card and lane rows alike, no dead zone.

Board: Minor card c9c4de2f → Resolved.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:59:58 -04:00
rzen 05331b4a87 Retire Tombstone from 11's ⌫ row; give Select All its trash clause
Two stale rows left by the trash rewrites: the ⌫ grammar row now names Delete staged by place instead of the retired tombstone model, and Select All carries 04's container-boundary rule.

Board: Minor card cb3bc015 → Resolved.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:59:23 -04:00
rzen 115409b553 Fail-fast is healer-gated — refusal is the last resort and no longer a dead end
User-ruled doctrine: a defect with a reliable heal heals and moves on (non-blocking notice at most); only an unhealable defect blocks, and blocking means a decision surface — load pauses, the user chooses the fix, the fix applies, the load proceeds. The loader collects every fail-fast defect in the walk rather than stopping at the first; one aggregated surface, never a modal chain. The surface's design (decision vocabulary, aggregation, reconciliation with the welcome failure row and the loading state) is filed as its own design pass — Redesign card a82a19f1 atop Issues to Resolve.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:58:46 -04:00
rzen b1db43137a Delete Immediately joins card and lane context menus as Delete's ⌥-alternate
10's inventory claim was right; 11 gains the rows Finder-style — hold ⌥ and Delete becomes Delete Immediately, surfacing as its own VO custom action per 10's cut. The trash needs no alternate since its Delete is already permanent.

Board: Minor card a69e5985 → Resolved.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:57:28 -04:00
rzen 683c3bfb76 03's toolbar Undo/Redo disable is edition-aware — no undo provider in the running edition
The parenthetical predated the edition split and read as base never having working Undo/Redo. Now conditioned on the provider: Pro disables on no-git and repo-nested boards; base's native stack serves every board, disabling only under locks and empty stacks.

Board: Minor card 7e43562e → Resolved.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:56:31 -04:00
rzen feae6d07d6 Copies are transactions; coerce-tier fallbacks log — and the leniency doctrine is stated
Two rulings (2026-07-29). Copy flows that stamp descendants preflight the whole subtree and refuse loudly naming the offender — the nested-uneditable silent skip is reversed; nothing partial ever lands, and post-preflight failures clean up wholesale per the construct-then-clean precedent; whole-board forks stamp nothing and stay exempt. No-sensible-reading fallbacks now log field, path, and raw text as coerce-tier Defect-stream entries — the one silent-and-unlogged recovery joins the family posture, and the stream is where real-world shapes get promoted to heuristic heals. The doctrine both rulings flow from is now stated in the taxonomy preamble: leniency recovers recoverable issues through reliable service-owned heuristics, never accepts loss that could surprise the user, and "proceed partially, lose a little" is never a verdict.

Board: Minor cards 70b3e4ba + fcc5a70b → Resolved. Code realign queued: stampCopiedDescendant leniency → subtree preflight; FrontmatterFields fallbacks → Defect stream.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:55:42 -04:00
rzen f80f838620 Both editions installed is a supported steady state — handler, races, clipboard ruled
Ruled 2026-07-29, new block in 12: Pro claims the .kanban default on first launch once (the purchase is the consent; a manual reassignment back is respected; base never claims). The same board open in both apps is the designed foreign-writer story — base has no git so there is no committer contention, no board-level lock (files-first stays absolute), no gate; open-now flags go per-edition on the shared record so restoration stays edition-local, and the popover gains a pid-liveness-checked "Also open in Lanework Pro" line. The clipboard staging store homes in the App Group container, making cross-edition copy/paste full-fidelity; the degraded fallback remains for genuinely missing snapshots.

Board: Gap card 7a244f9f → Resolved (Gaps lane now empty); App Group Implementation card gains the flags/staging/handler bullets.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:43:27 -04:00
rzen 480336bc69 Heal commits are transparent to undo — the bounce is bounded to one, post-relaunch only
Ruled 2026-07-29: heal-class commits (known in-session by heal-marked EchoLedger receipts) never become undo steps — the stack pointer passes over them and restores exclude heal-owned paths, so ⌘Z never reverts a repair and never re-arms the scheduler; naive acceptance would trap a ⌘Z run on an ever-renewing heal commit, and suppression would create standing invalid states. Post-relaunch reseed stays sidecar-free, so old heal commits bounce once — restore plus fresh heal commit, the fresh one transparent, the run continuing past it. Accepted family-wide; the agent-guide quirk becomes the rule's oldest case.

Board: Gap card b444bc5c → Resolved. Pro-era implementation (pro-m1).

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:35:03 -04:00
rzen 4c9528f158 Every open passes through a pre-snapshot loading state — the window appears immediately
Ruled 2026-07-29: board windows exist before their first snapshot. Immediate window at the saved frame with registry-cached chrome, quiet spinner after a 200ms grace (no skeletons), first snapshot snaps in, ⌘W cancels the walk as an ordinary close. Restoration goes parallel — all flagged windows at once, walks independent, slow volumes never serialize the launch. Failures keep the settled welcome row surface; the loading window retires rather than morphing into an error display. No info row — the loading state is the surface.

Board: Gap card 4d95f269 → Resolved.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:32:24 -04:00
rzen abadf11929 Displacement is level-uniform — a file squatting attachments heals like one squatting .trash
Extends the claimed-names displacement to a card's reserved child names: a wrong-kind node on attachments displaces by the same ladder, scheduled class, and notice, retiring the imports-fail-one-gesture-at-a-time mode one level down; the displaced file then rides the loose-file relocation into the real attachments/ — the heals compose. comments stays a tolerated stray until the tracker era consumes the name, per the load-bearing-now timing principle.

Board: Gap card 64a1c409 → Resolved.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:18:16 -04:00
rzen c42e2e5447 App-side state crosses editions through a shared App Group — homed there from day one
Ruled 2026-07-29: every edition declares group.dev.rzen.indie.Kanban, and the board registry plus its Application Support peers home in the group container from base 2.0 onward — no migration, no ordering dependency, and the entitlement ships in base's first build since base ships first. A paying upgrader launches Pro onto their full recents. Stated caveat: security-scoped bookmarks never cross sandboxes, so the list transfers while access re-grants per board — per-edition grant slots on the shared record, open panel pre-anchored at the recorded path. Teams joins the same team-scoped group later with zero ceremony. Clipboard staging store stays put pending the both-editions-installed clipboard ruling.

Board: Gap card a925e530 → Resolved; Implementation card a647aba2 filed atop Backlog (before-base-ships constraint).

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:16:52 -04:00
rzen 176c8520fc Reliably healable repairs run silently — the user-gated Repair banner retires
General ruling (2026-07-29): app-initiated covers every reliably healable repair — unambiguous, content-lossless — because heals are auditable: Pro's separate heal commit is the durable trail, base happens quietly, and a heal that changes user content announces with a warning-tone notice, never a consent gate. The user-gated class stays defined but has no current members. Duplicate-id healing goes silent under Lanework-owns-the-board (free to re-mint UUIDs at will): no condition banner, no Repair button — a scheduled heal remints withheld occurrences (memo-guarded, disk re-verified, no-op on vanished duplicates) and posts "Repaired duplicate id — 'Fix login'". Winner rule unchanged: earlier commit on git boards, filesystem birth date then traversal order without history. 06 keeps the Repair verb as the heal commit's name; 13 moves the remint outside undo with all heals. Dissolves the Repair-has-no-Nexus-row contradiction — no command, no row owed.

Board: Contradictions card ce5e7f60 → Resolved (lane now empty); Implementation card 50ed2b5d (Active) retitled "Dedupe duplicate ids and heal them silently" and realigned mid-flight.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 17:10:29 -04:00
rzen 1d1796a7f1 Moves don't stamp — order-only rewrites leave modified and modified-by alone
The rule 01 ▸ Deletion and 03 ▸ Trash cited is now stated (ruled 2026-07-29): order is logically the container's property, stored as an attribute in each member's file for implementation convenience, so reorders, cross-lane moves, and renumber rescales rewrite index.md without touching content — no modified stamp, no modified-by clear. The trash move is the genuine exception: deletion is an edit to the item's story, stamping and clearing both, feeding the future age-based auto-purge. Considered and declined: stamping the container's own modified on reorder — fair reading, pragmatically risky to enforce, nothing reads it.

Board: Contradictions card 30c8f207 → Resolved. Code realign (Writer stamp suppression, agent guide v7) queues for the session-end realign card.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 16:45:45 -04:00
rzen 0e4fc525e0 Purge the lanes-in-trash residue — 11's Delete rows, 06/07's card-only phrasing, 02's counts
The 2026-07-29 lanes-trash-too ruling never swept the docs last touched 07-28. 11 ▸ Delete drops "lanes delete physically" for the card-or-lane move (freight-counting confirms in the trash); Delete Immediately widens to any card or lane selection; 06's recoverability exception covers both kinds and drops the retired 13-native-undo citation; 07 reads card and lane deletes; 02's welcome-counts exclusion restates as items in .trash/ (trashed lane subtracts from the lane count, its nested cards from the card count — the sibling Minor's ruling folded in). Context-menu row "Trash cards" renamed "Trash selection" on the same basis.

Board: Contradictions card 89a625bf + Minor card 488dc2c2 → Resolved.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 16:37:29 -04:00
rzen 3a9db2e78b Build the integrity service - IntegrityRules and the HealScheduler
The 2026-07-29 integrity design pass, consolidated (DESIGN/01 -
Validation and healing; DESIGN/02 - Components): IntegrityRules
(Storage, pure) is the one home for the identity predicate and
canonical form (BoardWriter.canonicalIdentity deleted, ItemID and the
loader forward to it), the per-field rulebook, uneditable shapes,
per-kind index validation, the reserved-name tables, and the trash
kind discriminator (values trusted - kind: lane/card explicit,
unrecognized falls to shape). LoadResult's ad-hoc channels fold into
one typed Defect stream (looseCardFiles / legacyTombstone /
claimedNameSquatted, per-defect heal signatures); the old accessors
survive as computed views.

HealScheduler (LiveStore) states the six-step heal pattern once -
resting-clear, lock gate, isWritableFile gate (now covering all four
heals), signature memo armed-before-attempt with explicit
clear-on-success, disk re-verify in each write half, one banner-posture
table (BannerCenter keeps all phrasing). The three hand-rolled healers
run on it with behavior preserved - including the
relocation-notice-despite-partial-failure quirk, deliberately. Heals
run at the reload tail AND at registry acquire, closing the
migration-never-fires-at-open asymmetry. Displacement runs first: a
squatted .trash would otherwise fail the migration and arm its memo
against an unchanged picture.

Claimed-name squatters (ruled today, 62c47a2) displace by the shared
Finder-style rename ladder - preserved verbatim, symlinks moved as
links, nothing stamped; AgentGuide's untouchable-skip upgrades to
displace-then-write, the CLAUDE.user.md-taken skip stands. kind stamps
on every create and backfills on any index rewrite via the on-touch
seam (placement resolver stamps nothing when the parent is unknown -
a guessed kind is worse than an absent one; board-root writers declare
theirs). Heal writes mark their EchoLedger receipts (inert in base;
pro-m1's committer will split them into their own commits). The
renumber ask-renumber-ask-again two-step is one shared helper, adopted
at all nine call sites.

69 tests added. 1738 green on both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 15:45:48 -04:00
rzen 0d846c634e Agent guide v6 - warn against lane-glob moves
A real agent incident (2026-07-29): moving cards with mv <lane>/* swept
the lane's own index.md along with the card folders, overwriting the
destination lane's identity file and leaving the source lane index-less
(the Implementation board briefly lost its lane titles; restored from
its auto-commit history). Both guide generations taught the correct
named-folder form but never said why it is load-bearing.

v6 adds the one-folder-at-a-time bullet to Moving and reordering:
a lane folder holds its own index.md beside its cards, so a glob
sweeps the identity file with them. Boards heal to v6 on their next
open per the first-line marker rule. The guide tests now derive
their expected marker and current/newer versions from
AgentGuide.version instead of a hardcoded literal, so the next bump
cannot silently break them.

1669 green on both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 14:32:31 -04:00
rzen 62c47a2209 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
2026-07-29 14:24:07 -04:00
rzen bf559bbbb8 Healing mutations commit separately — the heal class of the two-commit split
User ruling on the integrity pass: a scheduled heal's batch never folds into
a commit carrying anyone else's work. The committer's two-commit split gains
a heal class keyed by heal-marked EchoLedger receipts; the composer stays a
pure diff reader and names heal commits from their own diff shape. On-touch
heals ride their host write's commit (the backfilled kind joins the
bookkeeping no-event class), inline heals batch with their gesture, and the
user-gated Repair already commits alone.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 14:09:28 -04:00
rzen a2bde31290 Promote kind to the common schema — written at creation of every object
User addendum to the integrity pass: kind moves from the lane table to the
common frontmatter table — board/lane/card at creation, comment reserved;
value-trusted where consulted, consequential only inside .trash/ today,
carried everywhere for consistency. A missing kind backfills on touch via
the integrity service. Guide v6 teaches kind on every object.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 14:05:16 -04:00
rzen 1b02f6e063 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
2026-07-29 13:59:46 -04:00
rzen aad8857ea0 Set up distribution and release
Records the channel decision: the Mac App Store, as version 2.0 of the
existing dev.rzen.indie.Kanban record - the 1.x app already listed
there under the Lanework name. The question the card held open was
settled by three prior rulings (the edition split's separate App Store
targets with base keeping the shipping bundle id, the 2.0 marketing
version, and the account's App-Store-shaped tooling); RELEASE.md
records the reasoning and why notarized-direct is not pursued for 2.0.

Pipeline carried over from the 1.x repo: scripts/release.sh (archive +
destination=upload export, --platform macos reminders in its epilogue),
ExportOptions-macOS.plist, .env.release.example (+ .gitignore entry for
the real credentials). scripts/metadata/ holds the fresh 2.0 listing -
rewritten description/what's-new for the rewrite (plain-files boards,
live outside edits, card windows, trash, undo, templates, agent guide,
accessibility), carried keywords/URLs/categories/pricing (free)/
availability (ALL), and review notes updated to the 2.0 UI. All fields
within API limits; asc-metadata --dry-run validates end-to-end against
the live record (version 2.0 would be created; pricing and availability
already correct). Local Release archive verified: Lanework 2.0 build
165, signed, correct identity.

Remaining steps are deliberately user-gated at RELEASE.md's PUSH POINT:
screenshots need a real display, and everything past the archive writes
to the live record of a shipping 1.x app.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 13:43:26 -04:00
rzen f34707e17e Build the end-to-end UI test pass
The golden-path XCUITest suite, adapted to current rulings where the
card body had gone stale: trash flows follow the materialized-trash
grammar (no Put Back, restore is drag or cut/paste out), git flows are
pro-m1 scope and skipped, and fixtures extend m11's in-container
--ui-test-fixture-board mechanism (the sandbox forbids the card's
--open-board path handoff) with exact-match variant flags: standard
(the three-lane audit board), large (8 lanes x 40 cards for
masonry/reflow), malformed (BoardWriter-built board with one card's
index.md overwritten to unterminated YAML, opened through the ORDINARY
path so the failure is the loader's own).

EndToEndFlowTests: create card/lane, inline rename, coordinate drag
across lanes, cut/paste, undo/redo of a move, delete-to-trash /
show-trash / restore-by-cut-paste / Empty Trash confirm - all asserting
on lane accessibility labels. FailFastLaunchTests: welcome appears, no
board window ever, a welcome row carries the loader's sentence naming
the file; byte-fidelity of the malformed board pinned unconditionally
in KanbanTests plus an identically-refused relaunch. Performance:
launch metric plus explicit wall-clock gates (30s launch / 5s Show
Trash on 320 cards) since XCTest baselines don't travel. Powerbox
panels (template save panel, Duplicate fallback, Open) are documented
as manual in EndToEndVerification.md, not faked.

The smoke test now launches on the standard fixture (it launched bare
before, opening the developer's real boards); README's everyday test
command scopes to -only-testing:KanbanTests.

Suite compiles on both schemes (build-for-testing verified); flows
await a real display + automation permission to execute - run
instructions in KanbanUITests/EndToEndVerification.md. +8 unit tests;
1669 green both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 13:40:18 -04:00
rzen 71b112d04c Card drag preview renders at the source face's laid-out width
The replica framed to a font-derived nominal width, so faces in wide
lanes' interior masonry columns and narrow lanes dragged a preview of
the wrong size and the cursor could sit over empty space beside it.
CardFaceView's existing onGeometryChange now reports size, not height
alone - height still feeds LaneDropRegistry, width feeds the replica -
and BoardMetrics.cardReplicaWidth(measured:bodyPointSize:) keeps the
old figure as the documented not-yet-laid-out fallback. Same row, same
paddings, same lineLimit at the same width means the same height, so
the preview is now an exact overlay of the face it left.

Manual verification pending alongside the lane grab-point card: 3x vs
1x lanes, interior masonry columns, window re-divide, trash rows,
large text sizes, drag-at-creation fallback.

3 tests. 1661 green on both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 13:02:57 -04:00
rzen f83385e79f Lane drag preview attaches at the grabbed title bar, not its middle
SwiftUI's .onDrag(_:preview:) exposes no anchor or grab-point API and
lays the preview centered over the grabbed view, so a full-height lane
replica grabbed by its ~1.5-line title bar hung half a lane above the
pointer. DragPreviewAnchor is the pure fix: transparent one-sided
padding computed so the padded image's center IS the replica's title
bar - the system's centering then lands the bar under the cursor and
the body over the lane it was lifted from. LaneView measures the real
bar height in the geometry observer it already runs, accounting for the
accent band when the lane's color resolves.

Pixel-exact grab preservation would need re-homing the whole gesture
stack onto an AppKit beginDraggingSession path; declined for a polish
card - this gets the cursor onto the grabbed bar under either of
SwiftUI's possible placement rules. Manual verification pending (no
display here): grab specific bar pixels, colored + uncolored lanes,
large text sizes, multi-lane fan, cross-board drop geometry.

9 tests. 1661 green on both schemes with the paired card-width fix.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 13:02:57 -04:00
rzen 28ef9eef7e Create the app icon and finalize versioning
App identity for both editions: the three-lane glyph
(rectangle.split.3x1.fill) over a gentle gradient - teal for base
Lanework, dark graphite for Lanework Pro - rendered as 1024 masters and
packaged down the full macOS ladder into each target's own iconset,
replacing the borrowed pathfinder icon.

The About window arrives via IndieAbout (0.2.x): icon, copyright,
version/build/date read from the Info.plist that update_build_info.sh
already stamps on every build (CFBundleVersion 160 = git commit count,
BuildDate, BuildHash - verified in the built product), the version line
opening the bundled CHANGELOG.md, and the new ISC LICENSE.md one
document link away. Both files are authored to the Apple
inline-Markdown subset (no # headings, single-line paragraphs) and
bundle into both editions.

CHANGELOG.md replaces its placeholder with the real 2.0 first-release
notes - one sentence per user-facing feature.

The About box is also an edition seam: base's EditionAbout carries the
one-line Pro signpost 12-editions.md allots ("Lanework Pro adds
git-backed board history and sync."), and the Pro target compiles its
own signpost-less twin instead of that file - the no-#if file-level
doctrine applied to a string. README gains the identity bullet.

1649 green on both schemes; verify-editions.sh 26/26.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 12:43:26 -04:00
rzen 1c263b9f2e Lanes delete into the trash — the design pass
User-proposed and ruled 2026-07-29, retiring the design's sole
destructive delete: lane delete becomes a move into .trash/, exactly as
cards move. Full rationale on the Redesign board's Resolved card;
implementation is post-m12 ("Lanes delete into the trash" on the
Backlog) — the docs deliberately lead the code until then.

- 01: kind: lane always present on lane frontmatter (created with the
  lane, healed on touch, never stripped) — discriminates lane from card
  in the flat .trash/, where an empty lane is shape-identical to a
  card; legacy deleted: on lanes is now simply ignored (no migration,
  no notice — backward compatibility deliberately not funded).
- 03: trashed lane = opaque dimmed row with card count, restores whole
  or purges whole; confirms count lane freight; fixed 03's own copy of
  the "native undo restores a permanent delete" contradiction.
- 04: delete staged by place for lanes too (board→trash no dialog,
  trash→permanent confirmed); lane drop on shown trash deletes;
  kind-homogeneous trash selections, ranges skip by kind; ⌘X/⌘V restore
  rides the existing lane-paste rule.
- 10: trashed lane is one flattened element, never a container.
- 13: lane delete inverse = ordinary move back; recreate-from-capture
  retires — no destructive delete remains outside the trash.
- 08: agent guide v6 — lanes trash by move, kind: lane taught.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 12:42:49 -04:00
rzen 89d4d983e6 Wire the open-time writability probe and read-only lock
Closes the gap found at m10: enterUnwritableLock existed with zero call
sites. WritabilityProbe classifies the cause volume-first - a board on a
read-only DMG is also permission-denied by access(2), and "you don't
have permission" would send the user to a Get Info panel that cannot
help - with a pure classify(volumeIsReadOnly:isWritable:) truth table
and a two-syscall probe that rebuilds its URL to defeat NSURL resource
caching. ReadOnlyLockReason.unwritableLocation now carries the cause;
BannerCenter phrases the two ("this board's volume is read-only" vs
"you don't have permission to change this folder").

The probe wires once in BoardStoreRegistry.acquire, immediately after
the store loads - every open path funnels through it, and running
before the loose-file relocation and agent-guide hooks makes the
skipped-with-log guide write true by construction (its isWritableFile
pre-check demotes to second line of defense). The board still opens:
lock, not refusal.

The reconciling re-probe is now symmetric per 02's settled text - a
volume gone read-only mid-session raises the lock at the next probe
(sibling locks settle first, so a root returning read-only lands the
honest lock); the stale "deliberately one-way" comment and its pinning
test are gone. Save as Template's carve-out predicate extracted to a
testable allowsSave (behavior unchanged); Duplicate stays disabled.

11 tests added. 1649 green on both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 12:34:55 -04:00
rzen 5880838e66 Build the EchoLedger - per-file write provenance for announcements
User-ruled 2026-07-29: the ledger builds now in base, pre-release
(DESIGN/02 - Components - EchoLedger; DESIGN/10 - Live board
announcements). Receipts drop inside BoardWriter's four disk primitives
(atomic replace, folder move, removal, attachment copy) into a @TaskLocal
ledger that BoardStore.performWrite binds for the bracket's duration -
no call-site bookkeeping, and performWholesale deliberately binds
nothing per 02's bracket exemption. Classification is a pure function of
two snapshots: an item whose folder, index.md bytes, or attachment
listing differs is an observed change; disk matching the receipt is
app-mediated (receipt consumed), no receipt or mismatch is foreign.
Byte-identical foreign overwrites classify app-mediated (unobservable,
accepted); a foreign edit over a fresh app write classifies foreign.

The announcer now consumes per-file facts on every reload origin - the
WatchOrigin gate is gone (ReloadFacts.origin removed outright; nothing
read it after the gate fell). Reconciling sweeps announce their
receipt-less findings as foreign, closing both interim holes
(debounce-window absorption, reconcile silence). The vanishing-focus
rung gates on the ledger too: "deleted externally" would be a lie about
an app-mediated delete, and the subject's own verdict decides.

Divergence flagged: attachment imports hash the landed file right after
FileManager.copyItem rather than during the copy (the bytes do not
stream through the app); an unreadable read-back records nothing, the
direction that biases toward foreign.

30 ledger tests added, announcer suite reworked to the ruling. 1638
green on both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 12:14:47 -04:00
rzen 28ca2c3f50 Realign code with the 2026-07-29 accessibility rulings
Three ruled behavior changes (DESIGN/10, resolution session 2026-07-29):

- The board-change digest covers the trash while View > Show Trash is on:
  BoardDiff.between gains includingTrash, keying its card index by
  ItemPath so foreign purges, restores, and Empty Trash join the digest;
  crossings of the trash boundary still read deleted/restored, never
  moved, on both sides of the toggle. BoardStore.land passes the store's
  own isTrashVisible - no new injection seam.

- A vanished head with surviving co-selection is still named: naming and
  recovery are independent axes, so BoardAnnouncer's vanished-focus rung
  fires on all branches while the survivors-veto now gates only the
  recovery half (recovery implies vanished, no longer both-or-neither).

- Banner-row buttons are literal FKA Tab stops: BannerRow.controls is
  the row's testable button inventory, BannerRowView renders from it
  with .focusable() on each button; the combined VoiceOver element stays
  unconditional - custom actions and Tab stops are independent surfaces.

19 tests added, 2 expectations updated to the rulings. 1607 green on
both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 11:37:46 -04:00
rzen 065c6f0678 Rule the EchoLedger into base pre-release
User-ruled 2026-07-29: the ledger's construction slot returns to base
("right first") rather than riding with pro-m1's auto-committer. 10 now
pins that the announcer consumes per-file facts on every reload origin —
reconciling reloads' receipt-less changes classify foreign and announce
(the launch-catch-up doctrine applied to speech), closing both interim
holes: debounce-window absorption and reconcile silence.

Implementation filed as "Build the EchoLedger" (order 65920) on the
Backlog; pro-m1's auto-commit card annotated to consume, not construct.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 11:33:25 -04:00
rzen 95f00211c1 Resolve the 2026-07-29 design-review findings into the DESIGN docs
Settles the m11-accessibility findings and two undo-doc findings from the
Redesign board (rationale per card in its Resolved lane):

- 13: permanent delete stays non-undoable — the trash-section clause
  contradicting Rules is amended; staleness validation reads disk, not
  the snapshot (blessing the implemented false-skip fix).
- 10: VO-Space toggles on any selectable element; the digest covers the
  trash while shown; a vanished head with surviving co-selection is
  still named (naming and recovery are independent axes); banner-row
  buttons are literal FKA Tab stops; lane-title landmark+heading
  doubling accepted; trash visibility wording pinned; the custom-action
  cut confirmed; banner transitions are origin-independent; the AA
  obligation binds the primary label tier.
- 03: the palette AA claim rewritten to the real mechanism — one
  ink-selection code path for palette names and hex, pinned by
  PaletteContrastTests in both appearances.

Code-conformance residue filed as "Realign code with the 2026-07-29
accessibility rulings" atop the Implementation Backlog.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 11:10:59 -04:00
rzen c5edcd8528 Run the accessibility verification pass
The automated half: AccessibilityAuditTests runs performAccessibilityAudit
over all eight surfaces DESIGN/10 names — board with trash hidden and
shown, card window in Preview/Edit/raw source, welcome, template chooser,
board popover. One audit per test, .all audit types, no issue handler —
nothing waived; a future false-positive excusal must match one element on
one surface with its reason beside it. Navigation is menu-bar titles and
the arrow grammar; no accessibility identifiers added to production code.

The suite launches with --ui-test-fixture-board: the sandbox forbids
handing the app a temp-folder path (no bookmark behind it), so the flag
carries no payload and the app builds a known board inside its own
container through the ordinary BoardWriter door — three lanes, six cards,
a rich Markdown body with attachment, one card already in .trash/ — with
the registry redirected to the same scratch directory so audit runs never
pollute real recents. LaunchPlan replaces the restore Bool (welcome /
restoreBoards / uiTestFixture, fixture wins outright), decided once in
KanbanApp.init and dispatched by RestoreBootstrapView; pure and pinned by
UITestLaunchTests, and the fixture itself is materialized and read back
through BoardLoader in units — the only proof available headlessly.

The manual half: KanbanUITests/AccessibilityVerification.md is the one
document — the audit suite at the top (it needs a real display and
Accessibility permission), the per-release VoiceOver smoke script with
expected utterances quoted from AccessibilityPhrases, and the
consolidated m11 checklist from all four implementation cards.

1588 unit tests green, UI target compiles, both schemes build. The audit
run and smoke script await a real display — the manual pass is the
user's.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 10:42:14 -04:00
rzen 92a088fdd3 Runtime contrast for hand-written hex backgrounds
DESIGN/10's ≥ 4.5:1 rule gets its owner. ContrastMath is the pure seam:
WCAG relative luminance (piecewise sRGB linearization), symmetric
contrast ratio, source-over compositing (an #RRGGBBAA board colour
resolves over the window background of the active appearance), and
inkChoice — native label if it clears AA, else the other appearance's,
else the higher ratio with meetsAA false (a mid-grey hex can max out
below 4.5 against both 85%-alpha labels; the app paints the best
available rather than overriding the user's colour). BoardTextInk is
the board's application: the decision is a ColorScheme, not a Color —
the text on the board fill is a hierarchy (.primary/.secondary/
.quaternary), and overriding the subtree's scheme moves the whole
vocabulary coherently. Recomputed on appearance change by construction
(read in body); label/backdrop colours resolve inside the asked-for
appearance, Increase Contrast variants included.

Two render sites — the only board text that sits on the user's colour:
the lane header (lanes draw no plate; title, icon, badge, rename field
and the + button land directly on the board fill) and the trash header
(its wash is ~5% effective alpha). Menus, popovers, and drag replicas
deliberately stay native; card faces carry their own opaque plates.

The card's premise fell during implementation: 03's "palette pairs
AA-verified at design time, pinned by a computed-contrast unit test"
was false — no such test existed, and the m4 path drew the native label,
failing AA in one appearance for all twelve wells (obsidian in Light
Mode: 1.0:1). Palette names now route through the same ink selection
(paintedColor delegates to Palette.nsColor — one predicate with
BoardView's paint decision), and PaletteContrastTests pins that the
chosen ink clears AA for every well in both appearances — plus
theNativeLabelIsNeverEnough, which would have failed on the m4 code.
Filed on the Redesign board for ratification. 1579 unit tests green,
both schemes build.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 10:20:16 -04:00
rzen 8564814754 Implement visual accommodations and Full Keyboard Access
Full relative text scaling per DESIGN/10: BoardMetrics is the board
strip's geometry as a pure function of the body point size
(CardWindowMetrics' twin) — lane plate/header/band, card
corner/stripe/padding, masonry spacing, the drop model's nominal card
height, resize-handle geometry, trash hatch pitch, and both window
floors all derive from an em; CardFaceMetrics folded in. The two fixed
font sizes (welcome brand/glyph) went relative; the toolbar search
field is 17 ems like the transient bar's. The no-horizontal-scroll
invariant is pinned by test at six text sizes by twelve lane counts.

Accommodations is Motion's sibling for the visual settings: Increase
Contrast adds a flat point to strokes (monotone, hierarchy-preserving),
gives borderless card/lane plates a resting separator hairline, and
takes faded accents to full alpha; Reduce Transparency turns the
transient search bar's glass solid and does the same for the alpha
washes that composite over a user-chosen board background (trash plate,
hatched header, drag shadow). Reduce Motion audited — every animated
surface already routes through Motion with a reduced variant; no gaps.

Full Keyboard Access: the template chooser's tiles were pointer-only —
now focusable, arrow-navigable (clamped, StyleWellGrid's rule), Space
picks, Return stays the sheet's default action, focus names the
selection one-way. The board's single tab stop shows its focus ring
under FKA (focusEffectDisabled inverts). Style editor verified already
conformant. Edge accents verified text-free; trash hatch pitch now
font-derived so it still reads as hatching at large text.

1549 unit tests green, both schemes build.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 08:48:25 -04:00