Commit Graph
309 Commits
Author SHA1 Message Date
rzen 6c490ec71c Build per-surface context menus
The remaining rows of 11-command-nexus.md § Context menus, every entry a
twin of an existing command path, never a parallel implementation:

- Card: Open (the double-click's own openCard closure, always the clicked
  card alone), Rename (Board ▸ Rename's beginRename path), Style… and the
  quick-style recents (already present), Delete (File ▸ Delete's
  store.delete on the standard widened target — selection when the clicked
  card is a member, else the card alone).
- Lane (one menu, header and empty space): Rename and Delete join the
  existing Style…/recents/Width rows, in table order.
- Trash entries and welcome recents verified already exact against the
  table; the attachment row's menu is marked for m6 beside its command.
- File ▸ Reveal in Finder gains its board-window scope, the branch the m4
  comment deferred here: the selection's folders on either side of the
  trash boundary — enabled under every lock, inspection being a read — or
  the board root with nothing selected; a selection resolving to no
  folders disables rather than guessing.

904 unit tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 23:42:37 -04:00
rzen c1f304d3fe Build the menu bar per the Command Nexus
Nexus parity, audited row by row (11-command-nexus.md § Menu commands) —
every already-built item's title, chord, and placement matched exactly;
this pass fills what remained:

- The future-window rows, present with stable titles and validation-driven
  disablement until their milestones fill the actions: Save as Template
  (m9), Add Attachment… ⇧⌘A, Find Next/Previous ⌘G/⇧⌘G, the View-menu
  card triplet Edit Body ⌘E / Raw Source ⌥⌘E / History (m6), Board ▸
  Pull/Push (git milestones) — one shared disabled-row shape in
  FutureCommands.swift so later milestones only flip validation.
- No Print story in v1: the print group is removed.
- Help carries the Nexus's one remap-teaching line — Customize Keyboard
  Shortcuts…, opening System Settings' Keyboard ▸ Shortcuts extension
  directly (the modern extension URL, verified to launch the appex).
- The launch-restore decision now runs through a pure, tested
  AppModel.shouldRestoreAtLaunch gate; the Settings pane's caption rides a
  proper Form section footer.

904 unit tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 23:28:22 -04:00
rzen cf87b72092 Implement live search filtering
The board's live title+body filter per 04-interactions.md § Search:

- SearchFilter — a pure value folding the query once (case- and
  diacritic-insensitive substring, locale-stable); title OR body matches,
  attachment filenames never searched; only the literal empty string is
  inactive.
- One universe: the filter threads through SelectionGrammar's order lists
  as a defaulted parameter, so ranges, Select All, arrow navigation, the
  marquee, drop zones, count badges, and the shown trash all read the same
  filtered set by construction; lanes are deliberately never filtered out
  (an emptied lane keeps its slot with a 0 badge). Hidden cards leave the
  selection through the existing constrain primitive, run on every query
  change and as the last line of the reload resolve; the delete successor
  is filtered so ⌫ never selects a hidden neighbour.
- The field: an NSSearchField-backed toolbar item (the toolbar's sole
  default item); Edit ▸ Find ⌘F focuses it through a focused-value
  presentation; stock field-editor dispatch — Return swallowed, Tab is the
  keep-filter path to the board, board commands stay enabled except the
  caret-chord pair, now one shared caretChordsYield expression.
- Escape is staged: clear the non-empty query (focus stays), hand an empty
  field back to the board, clear an active search from board focus —
  before Escape's clear-selection meaning.
- Creating a card clears the search (the placeholder funnel); a rename
  deliberately gets no carve-out; filter reflow rides the content spring
  keyed narrowly on the query.

903 unit tests (24 new).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 22:49:15 -04:00
rzen 7eee0934ee Implement the hybrid clipboard with deferred cut
⌘X/⌘C/⌘V for cards and lanes per 04-interactions.md § Clipboard:

- ClipboardStore stages full folder snapshots eagerly at the gesture into
  Application Support (at most the current copy; sweep at launch and on
  each copy purges what the pasteboard no longer references; a copy made
  before quitting pastes whole after restart) and writes the pasteboard a
  JSON manifest — every entry embedding its index.md, lane entries their
  cards' too — plus plain-text titles.
- Cut is Finder-style deferred: items dim in place off pendingCut, void on
  pasteboard takeover (changeCount, no timers), source-board close, or
  per-item external tombstoning; the first armed paste moves the surviving
  originals whole (tombstoned interior cards land in the destination's
  trash), a second paste materializes copies from staging.
- Paste anchors by the shared flatten-order rule (NewCardTarget's anchor,
  extracted); a tombstoned selection never anchors; lane paste reaches the
  right end and stays enabled on a zero-lane board; paste into the source
  board is the within-board lane duplicate; copies keep created, take
  fresh GUIDs, and strip tombstoned cards; trash-sourced copies strip
  deleted: at materialization; ⌘X is disabled on the trash side.
- A degraded paste is loud, never silent: staging gone → the embedded
  index.md fallback lands content-intact, attachments absent, and a
  BannerCenter-phrased row names what was lost.
- The standard Edit items validate through conditionally-attached
  onCommand handlers, so AppKit's enablement mirrors the availability
  predicates; text fields keep their own clipboard while focused.

879 unit tests (68 new).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 22:18:58 -04:00
rzen 8f116934b4 Implement Finder file drops
Files from Finder land on the board per 04-interactions.md § Drag & drop:

- Dropped on a card, they copy into its attachments/ (any type, multi-file),
  the face highlighting while hovered; the hovered card is resolved by
  hit-testing the same analytic masonry frames the card zones are built
  from, so attach-beats-create adds no drop region and cannot drift from
  the dispatch.
- Dropped on lane empty space, one card per file — filename minus extension
  as the title (a blank stem omits the key), fresh GUID, rank at the drop
  position through the ordinary insertion machinery, the file attached —
  all in one bracket; a failed import removes the just-minted card, so
  creating-then-abandoning never leaves an empty card behind.
- Every board drop surface now declares .fileURL beside the two board
  types (the single-target-dispatch rule); tombstoned surfaces are inert;
  file sessions ride a distinct session mode with their own watchdog and
  no hysteresis, leaving the board-drag machinery untouched.
- Also: four empty fixture directories pinned with .keep files so git
  preserves them, and a test-only visibility fix in DragSessionTests.

811 unit tests (12 new).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 21:20:46 -04:00
rzen 90cf82d740 Implement drag & drop with the locality model
The second half: system drag sessions over phase 1's model, per
DRAG-REORDER.md and 04-interactions.md § Drag & drop.

- Card faces, lane headers, and trash rows drag as NSItemProvider sessions
  (two exported UTTypes, JSON payload in flatten order, plain-text titles as
  the secondary representation) — replacing m4's custom lane-reorder gesture
  and trash drag-out wholesale; the app-wide DragSession carries the members,
  the frozen dragged sizes, the live proposal, and the effective operation.
- Three drop delegates (lane masonry, strip, window fallback), each accepting
  both types and routing internally per the single-target-dispatch rule; the
  cursor is the physical mouse converted to strip space; proposals come from
  DropSlotMath with hysteresis threaded through, and the lane-strip proposal
  clamps in front of the shown trash.
- Locality picks the default — move within a board, copy across, the badge
  tracking live; ⌥ forces copy (ignored on within-board lane drags), ⌘
  forces move; trash rows restore within their board (positional), copy out
  across boards by default, ⌘ forcing the true restore-move.
- N contiguous shadows with reflow keyed on the proposal; the
  committed-overlay hold renders the dropped arrangement until the reload
  echo lands (1.5 s dissolution deadline for refused writes); the
  re-grounding trio: geometry re-derives per render, proposals re-validate
  by liveness at release, an emptied drag cancels itself.
- Edge autoscroll (ticking driver over DragAutoScrollMath, re-targeting per
  step), the mouse-up-gated late-event cleanup, and the polling watchdog —
  the pathfinder's lifecycle traps, ported.
- Store: moveLanes and multi-card restoreByDrag join the one-bracket drop
  commits.

784 unit tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 20:58:26 -04:00
rzen f2d9f3ad07 Give the registry live display-state write-through and record-before-load
BoardRecord carries icon/iconColor; recordOpen/recordClose stamp them with the
display name, and a displayStateDelegate on the store (wired in BoardWindowHost
beside onFrameChanged) syncs all three through BoardRegistry.syncDisplayState on
every successful reload — welcome rows now wear the board's own icon and follow
in-app renames live. recordOpen now runs before the load with the folder name as
a brand-new record's provisional display name, so a first open that fails
fail-fast still lands in recents carrying the failure row-level (02's rule); an
existing record's cached name survives a failing retry, and the welcome fallback
list remains only for failures naming no record at all.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-27 20:44:30 -04:00
rzen cacd48cb0f Ratify the edition split and the 2026-07-28 design-review resolutions
Edition split follow-through: 12-editions.md and 13-native-undo.md join the
corpus; git-era docs (06, 07) are Pro-scoped and cross-references realigned.

Resolution session rulings written in place: caret chords yield to any focused
text control (04, 11); the drop settle holds its proposal as overlay state and
02 gains the overlays family; Duplicate gets the save-panel fallback and a
cancellable copy walk (03); Finder open is a standard document open (02);
failed first opens record before loading with the folder name provisional (02);
trash pointer ranges skip by kind, full-height marquee surface, Select All by
kind, rows-only trashed universe (02, 04); Empty Trash counts entries (03);
create handoff reads as one arrival (02); range-anchor lifecycle, board-
background click grammar, jump landing cards, lane-domain shift-arrows (04);
Reduce Motion restated per voice (10).

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-27 20:29:32 -04:00
rzen edc094a5f4 Realign the width and lane-move commands — three fresh settlements
DESIGN edits landing from the parallel session, honored:

- Caret chords yield to any focused text control (04 ▸ Grammar): Move
  Left/Right ⌘←/⌘→ and the width pair ⌥⌘←/⌥⌘→ now disable while the board
  popover is open — its fields are the one non-inline text surface a board
  window has today; the search field and card-window fields extend the rule
  with their own cards.
- The width pair batches over a multi-lane selection (03 ▸ Lane, the styling
  precedent): each selected lane steps one unit through one bracket
  (stepLaneWidths); floor members hold on a decrease; the context-menu
  stepper stays single-lane by nature.
- A width write landing on 1 removes the width key (03 ▸ Lane, the
  remove-at-default family), every mechanism alike — writeLaneWidths is the
  one commit point; a hand-written width: 1 is preserved by the unchanged
  guard until the app itself next edits width.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 20:10:59 -04:00
rzen 21a5a6dbfd Build the drop-slot model and the drop commits — drag & drop, first half
The pathfinder's drag-reorder model, ported and generalized (DRAG-REORDER.md
travels with it, rewritten for lanes, the interior masonry, multi-drag,
cross-board sessions, the re-grounding trio, and the committed-overlay hold):

- DropSlotMath — resting-layout zones from analytic lane arithmetic and the
  pure masonry placement (MasonryLayout now lays out through the same
  MasonryPlacement the drag reads, so geometry cannot drift), span-capped
  triggers sized to the dragged run's future footprint, hysteresis holds with
  the fresh-entry fallback, boundary ties, own-slot no-ops; nil means hold.
- DragAutoScrollMath — the activation bands and velocity ramp, pure.
- The drop commits, one performWrite bracket each: moveCards/copyCards within
  a board (insertion ranks touch only the dragged cards; renumber fallback);
  receiveCards/receiveLanes/receiveRestoredCards on the destination store for
  cross-board copy and ⌘-move with the import-boundary remint, lane copies
  stripping tombstoned cards while moves carry them; restoreByDrag is now
  positional, writing order only when the drop names a new one.

Gestures, sessions, previews, and delegates are the second half.

773 unit tests (87 new since the keyboard grammar).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 20:10:24 -04:00
rzen 4035ba7986 Implement the keyboard grammar and full command map
The board's fixed grammar keys and the menu-backed chords of
04-interactions.md § Keyboard, per the Command Nexus inventory:

- Spatial arrow navigation (NavigationMath.nearest over the marquee
  registry's frames — one geometry source), walking across interior
  masonry columns, lanes, and into the shown trash; ⇧-arrows extend via
  the same range function as ⇧-click and go inert at the liveness and
  kind boundaries; ⌥-jumps with the ⌥↑ lane-domain escalation and ↓
  descent; the empty selection seeds at the first lane's first card;
  selection scrolls into view.
- selectionHead — the navigation cursor beside the anchor, set by every
  click, moved by every arrow, dropped by the reload vanish rule.
- Board ▸ Open Card ⌘↩ (the one command enabled mid-edit: commits the
  placeholder or rename and opens), Move Up/Move Down ⌥⌘↑/⌥⌘↓
  (within-lane sort, gather-then-step, rank-permuting writes in one
  bracket), Move Left/Move Right ⌘←/⌘→ (sole lane, one slot, never the
  trash) — all validating and acting off one shared answer.
- Delete now selects the Finder-style successor sibling from the
  pre-write snapshot, so repeated ⌫ walks down a lane; external
  vanishing still only shrinks the selection.
- handleReturn rejects modified Returns; the trash column renders
  eagerly so every row stays registered for navigation and the marquee.

686 unit tests (27 new).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 19:32:30 -04:00
rzen 2e229735b1 Implement the selection model
The full pointer grammar of 04-interactions.md § Selection, stated once
as a pure function (SelectionGrammar) and reached through one store
funnel from every click surface — card face, lane header, lane empty
space, trash row:

- Plain click replaces and anchors; the lane surfaces (header and empty
  space alike, per the settled one-lane-click-behavior rule) toggle off
  on a sole-membership repeat.
- ⌘-click toggles within a homogeneous set; crossing any axis — cards
  XOR lanes, live XOR trashed, card entries XOR lane entries in the
  trash — degrades to a replace, so no click can produce a mixed
  selection.
- ⇧-click ranges from the anchor in the (side, kind) order list: flatten
  order for cards, lane order for lanes, the trash's deterministic sort
  filtered to kind — the pointer twin of the keyboard's boundary rule
  (the keyboard goes inert, the pointer skips).
- The rubber band (MarqueeSession/MarqueeMath) arms from lane empty
  space, the board backdrop, and the trash column; side frozen at the
  origin, trash bands homogeneous by topmost kind, frames self-registered
  in strip space, geometric begin guard, never animated.
- Fast plain double-click opens the card window (⌘↩'s pointer twin);
  Select All answers the standard Edit menu item via the responder
  chain, trash- and kind-respecting.
- The range anchor lives in TransientBoardState beside the selection and
  obeys the same reload vanish rule.

659 unit tests (28 new in SelectionGrammarTests).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 19:00:23 -04:00
rzen 4b97ecf3f0 Build the welcome screen
The welcome window becomes the real thing: Xcode-style, hidden title
bar with background drag, branding and actions left, recents right —
rows carrying the board symbol, name, location, and the registry's
cached lane/card counts (stamped at close, never a scan at welcome
time), sorted by last opened. Launch failures surface row-level per
02: a failure joins its recents row as a warning caption, an
unresolvable bookmark renders unavailable with Forget its one
affordance, and only a failure with no row to carry it falls back to
a compact list; a board opening again heals its row. New Board
(Opt-Cmd-N) opens the Pages-style template chooser — shipped with
the single Basic template and the m9 seams marked — flowing through
the save panel into createBoard/createLane and straight into a board
window. Open Recent gains its submenu with Clear Menu (byte-identical
to forgetting every row, pinned by test), and File > Duplicate forks
the frontmost board to a Finder-style copy sibling: pending work
flushes first through the close flush's step two alone (sessions stay
open — 09's stated exception), every GUID and tombstone carries (the
whole-board carve-out from copies-remint), and the copy opens in its
own window while the original stays put. 36 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 16:50:39 -04:00
rzen 5ebf9fb90c Implement the motion language
One named home for every curve and duration — the two-voice split 03
fixes (snappy structural: drag reflow 0.18, delete 0.25, lane resize
0.2; smooth content-reflow 0.28), the scale-and-fade appear/disappear
transitions (cards 0.8, lanes 0.9), and a Reduce Motion variant on
every accessor (animations go instant, transitions go crossfade). A
post-migration grep holds the invariant: zero motion literals outside
Motion.swift. The animate-vs-snap split lands where Lanework's
one-way flow puts it: the store's reload seam. An app-mediated echo
applies its snapshot inside the structural transaction; foreign and
reconciling reloads — and every bracket-ending wholesale reload —
assign bare, because live-reload is the board becoming what's on
disk, not an event to perform. A window that merged foreign events
into an app-mediated span animates, deliberately: the ratified merge
rule makes it indistinguishable from a pure echo, and a test pins
that reading so a future mixed case fails loudly. The lane-reorder
reflow keys on the drop proposal alone (pointer tracking stays 1:1),
the resize session freezes its Reduce Motion answer at drag start so
the unit tick and the window resize can never disagree, and the m5
drag / m5 search / m7 undo voices are named seams waiting for their
call sites. 13 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 15:59:59 -04:00
rzen 3be38fcb47 Realign the Cmd-N target rule — a multi-selection anchors at its last member
The corpus just ratified the plural case: a multi-selection anchors
at its last member in flatten order (lane order, then card order —
the multi-drag order, the same anchor paste will use), so creation
follows the last selected card or appends to the last selected lane
instead of falling through to the last-active lane. One display-order
walk keeps the resolver pure; the sole selection is now just the
degenerate one-member case.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 15:38:34 -04:00
rzen 2090d742b9 Build the trash quasi-lane
Deletion becomes a two-stage, Finder-style story. File > Delete and
plain Backspace tombstone the live selection; View > Show Trash (no
chord — Shift-Cmd-T stays with the system's tab bar) reveals the
quasi-lane: trailing, one fixed width unit consumed only while shown,
hatched dimmed header, count badge, no new-card button, exempt from
resize and reorder alike. Its contents are a pure view over the
snapshot — the deterministic sort (deleted newest first, folder-name
ties, unparseable stamps oldest) interleaves card rows with a
tombstoned lane's single entry, whose count names what Put Back
returns; the ancestor walk is absolute, so an own-flag card beneath a
tombstoned lane has no row and recovery is deliberately two steps.
Put Back twins Delete on Cmd-Backspace with validation enabling
exactly one; restore fidelity is byte-perfect because nothing ever
moved. Delete Immediately confirms exactly where loss is real (every
board is mode-none today; the predicate names the git carve-out for
m7), Empty Trash always confirms with the true whole-board count,
and dragging a tombstoned card onto a live lane restores it there —
positional drops and cross-board locality arrive with m5's machinery.
The banner's delete phrasing drops "move to the trash" per the naming
constraint: board deletion says Delete, "Move to Trash" stays
reserved for the system Trash. 47 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 15:38:34 -04:00
rzen aa6aaf2a11 Build the board popover — title widget, rename, styling, git slot
The window-title widget arrives as a leading titlebar accessory — a
quiet chevron on board windows only, installed and removed by the
window controller's own attach lifecycle — anchoring the one
board-level surface as a transient popover (the board window
deliberately grows no toolbar item for it). Inside: board rename
editing frontmatter title only (the folder is never renamed; an empty
commit removes the key and the window title falls back to the folder
name), the embedded shared style editor permanently targeting the
board, and the labeled Git section that this milestone only reserves
— a mode-none explanation and a disabled stub where m7's add-git,
branch, remote, and authentication controls land. Cmd-I (File >
Board Info) toggles it per window through a focused scene value,
kept apart from board-scoped transient state since a titlebar
popover belongs to one window, not to the board. Escape reverts a
dirty rename field and falls through to dismiss otherwise; a foreign
rename resyncs the field only while unfocused. 12 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 15:08:06 -04:00
rzen c6298c2e41 Build the styling system and shared style editor
One style-editor component, anchor-agnostic: a background grid (None
well plus the 12 palette colors) and a curated symbol grid (the
pathfinder's five-dozen set, leading well removing the icon key for
the level default), selection-aware across cards, lanes, and the
board itself. Batch edits compute per-dimension state — uniform,
mixed (no well selected), or an off-palette value labeled verbatim
outside the grids — and choosing a well applies to the whole target
set as one write bracket, skipping no-ops per field. The popover
tracks its target set live per the freshly ratified rule: targets
re-resolve by UUID on every reload, a vanished target leaves the set,
an emptied set dismisses the editor, and nothing ever silently
retargets to the board. Anchors landing now: Board > Style
(Opt-Cmd-S) and the card/lane context menus, which also carry the
quick-style recents row (app-wide, persisted, capped at six, None
never recorded) and the lane's width control twinning the menu
chords. The styling system's other two renders arrive with it: a
lane's background paints the C7 top-edge band, the board's paints
the window content background — malformed values paint nothing and
stay byte-identical on disk. 31 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 14:47:26 -04:00
rzen bea6d02d1d Realign read-side rules — width range coercion, finite order, symlink pins
The design corpus ratified that ranges are part of a sensible reading:
an exact-integer width below 1 now coerces to 1 read-side (bytes
untouched) instead of reading as malformed — the width division must
never see a zero or negative unit — while a non-finite order (.nan,
.inf) is now the same loud malformed-order rejection as a non-numeric
one, guarded at the single point where the double arrives so loader
and Writer inherit it together. The symlink-never-traversed rule
turned out to be already enforced (the loader has filtered symlinks
ahead of the directory check since the first commit); it and the
copy-preserves-the-link-verbatim behavior are now pinned by tests,
alongside the two hostile shapes the corpus names (width: 0,
order: .nan). Five new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 14:21:03 -04:00
rzen b4c90838b4 Build card faces with edge-accent styling
The card face becomes real: leading SF Symbol (card default doc.text,
tinted by a valid hand-written iconColor — schema yes, control no),
title or the quiet untitled placeholder, and a quiet paperclip when
the card has attachments — title-only by design, no body excerpt.
Color is the settled K1 edge accent, not a fill: background paints a
4pt stripe down the left edge, resolved through the ported pathfinder
palette (12 icon tints + 12 backgrounds carried over verbatim, plus
raw #RRGGBB[AA]); anything unresolvable paints nothing and stays on
disk exactly as written. The snapshot now carries each card's flat
attachment names — the loader's one read inside a card folder, shared
with the Writer's listing so the m5 carousel and m6 sidebar can never
disagree on order (Finder order, the Writer's existing comparator).
The face keeps its top-aligned structure so the sole-selection
carousel can expand inside the card without moving masonry neighbors.
18 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 13:48:50 -04:00
rzen b35566e0fe Build lane chrome — title bar, badge, inline rename
The lane title bar becomes real: leading SF Symbol (hand-written names
render leniently, unknown ones fall back to the level default), title
or secondary untitled placeholder, a quiet count badge that counts
exactly the cards the body renders (so the m5 search filter is
followed by construction), and a new-card button. The whole bar is
the reorder drag surface — no grip — with click-vs-movement splitting
select from drag; a pure proposal function maps the drag to an
insertion index and release commits through the Writer's same-parent
degenerate reorder, compacting and retrying when midpoint precision
runs out. Clicking never edits: inline rename is Return on the sole
selected card or Board > Rename for either kind, a third transient
editor beside the placeholder that tracks its target by UUID, commits
on focus loss, discards silently when the target vanishes, and
removes the title key on an empty commit. The new-card placeholder
renders at last — the settled Cmd-N target rule (pure, tested) files
it after the anchor card, at a selected lane's bottom, or into the
last-active lane; Return commits and re-selects the lane, Cmd-Return
also opens the card window, and a failed create discards the overlay.
New Card / New Lane / Rename land in the menus with focused-editor
and read-only validation; rename gets its own WriteOperation case in
the banner vocabulary. 59 new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 08:52:24 -04:00
rzen ff3ba298f0 Build the full-visibility lane layout
The lane strip replaces the placeholder board: window width divides
across the lanes' width units (no horizontal scroll, no minimum width,
degenerate compression accepted), a lane of n units flowing its cards
into n round-robin masonry columns via a measurement-cached Layout.
Width has two deliberately opposite controls, both landing here: the
right-edge drag (ported verbatim from the pathfinder's ColumnResize)
freezes the 1x standard at drag start, snaps between integer widths
with the asymmetric shadow-leads tick and 10pt re-entry, grows the
window one standard width per snap so siblings keep their exact
pixels, and rubber-bands at the screen's visible frame — uncapped
otherwise; the Increase/Decrease Lane Width items (new Board menu,
Cmd-Opt-arrows) are the stepper's keyboard face and re-divide the
existing window width instead, never touching the window. Width
changes write through the new .resize WriteOperation ("Couldn't
resize…" in the banner vocabulary, which grows with the surfaces by
design); malformed width values render as one unit and stay untouched
on disk. 27 new tests port the pathfinder's resize-math suite onto
the uncapped range and pin the write path's fidelity.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 08:16:14 -04:00
rzen fccdf56cf4 Stand up the window architecture — welcome, board, card
Four scenes (welcome, restore bootstrap, board group, card group) with
system restoration disabled in favor of the registry's open-now flags:
set when a window actually opens, cleared only on user close, so quit —
and crash — leave exactly the restoration set behind. AppModel joins
windows to sessions (shared store, registry record, card refs, held
security scope); CloseFlushCoordinator pins 02's strict close order as
a seam-injected machine (card sessions end, windows drain, store
flushes, record stamps, teardown) with named slots where m6/m7 flushes
land. HostedWindowController proxies — never replaces — SwiftUI's
window delegate to intercept windowShouldClose for the flush, report
frames, and place saved frames onto live screens. Card windows are
(board path, case-folded card id) values: reopen focuses, and a
snapshot-pure fate function dismisses on delete, tombstone, tombstoned
lane, or cross-board move.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 07:47:11 -04:00
rzen 61e18c3dfa Second Minor batch — eight implementation-session follow-ups ratified
- 02: failed create discards the placeholder; a second creation is a
  click-away for the open draft (replace-with-discard, ⌘N unreachable);
  unwritable-lock banner names volume vs permissions; WriteOperation
  vocabulary grows with rename/body-save surfaces; one bookmark per open
  board threads through the open flow; in-progress banner class ratified
  newest-first; signpost lifecycle is the dismissable untimed one-shot
- 01: folder-name tie-breaks compare the canonical lowercase spelling
  everywhere (Ranks and trash sort)

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 21:21:35 -04:00
rzen 3245060822 Symmetric writability probe; loader dedupes case-spelled identity twins
Two follow-ups to this session's decisions: the reconciling reload's
writability re-probe now raises the unwritable lock as well as clearing it
(fail loudly once, not per gesture), and under the shape-only identity
predicate the loader deterministically dedupes case-spelled sibling twins
on case-sensitive volumes (lowercase spelling wins, else lexicographic
first; loser takes the stray posture) so a snapshot never carries
duplicate ids.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 21:18:48 -04:00
rzen 8cf954a8c0 Ratify the unquoted-colon rescue — read-side recovery of the classic slip
Researched against the Frontmatter implementation: the duplicate-key
rescue's retry-loop pattern (compose a placeholder-patched copy, derive
spans from original lines) extends directly. Ratified: a top-level key
line whose plain unquoted remainder trips "mapping values are not allowed"
reads as everything after the first ": "; quoted/flow remainders and
nested shapes stay fail-fast; bytes preserved, fully editable, heals on
first app write of the key; silent with a log line. Test obligation named
(no hostile-YAML case covers the shape today).

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 21:17:30 -04:00
rzen adfe87fdec Batch of eleven Minor design-review fixes across the corpus
- 02: welcome counts are live items only; failed first opens still record;
  duplicate-identity registry records merge silently (newest wins);
  BoardStore's FolderWatcher pointer reads "below"
- 11: lane-header new-card button inventoried (click names its target lane,
  overriding 04's ⌘N rule — cross-referenced there); attachments quiet-add
  affordance listed as a twin; Undo/Redo and Pull/Push rows gain the
  abnormal-state pause, read-only lock, and unresolvable-remote conditions;
  welcome scope added to Reveal in Finder
- 03/11: one lane context menu (header or empty space) with the full
  inventory, replacing the split rows
- 09: instantiated boards are never in git mode — actual mode follows
  detection at the destination (repo-nested possible)
- 08: the app never writes an existing CLAUDE.user.md; one-time rescue
  creation stated as the exception
- README: pathfinder path corrected to ../../Kanban

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 21:09:23 -04:00
rzen 23e761120f Give transient UI state an explicit home in the store
TransientBoardState, one per store, holds state by how a reload treats
it: item-referencing sets (selection, drag membership, pending cut)
share one shape and one constraint rule — members must exist in the
current universe — applied in two directions by one primitive, so the
search filter's hidden-cards rule and reload survival are one rule
expressed once; derived state is stored as its inputs only (the query,
never its result set); and the new-card placeholder is a lane-anchored
overlay with no UUID until commit, discarded when its lane vanishes or
tombstones, handed off when the created card's UUID appears. Trash
visibility rides along per-open, never persisted. The decision is
written back into DESIGN/02 § Changes from Kanban — the TBD is closed.

Full suite 352 tests in 64 suites green. Two findings filed.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
2026-07-26 21:06:26 -04:00
rzen 8285e19497 Define how non-snapshot files commit — whole-root staging, path-shaped events
The composer was a pure snapshot diff while the repo tracked files outside
the model (agent guide, strays, .gitignore), leaving stray-only windows
uncommittable and the guide's own subject underivable. Settled: the
committer stages the whole root with the tree as its commit condition; the
composer also receives the changed-path list, composing "Update agent
guide (vN)" (version from the marker line) and "Update '<path>'" events
that ride as body bullets under model-event subjects; attribution falls
out of the path-keyed EchoLedger.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 21:05:04 -04:00
rzen 3a0a5b5e7c Pause the remote half of the git surface in abnormal repo states
The detached-HEAD/mid-merge pause named auto-commit, Undo/Redo, and branch
controls but left Pull, Push, and push-on-commit live — against a dirty
tree whose clean-by-pull-time invariant the held auto-commit had falsified.
Settled: the whole surface pauses; the ahead/behind badge keeps counting
since a fetch is a read.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 21:03:17 -04:00
rzen e618658bda Cede ⇧⌘T to Show Tab Bar — window tabbing stays enabled, Show Trash unchorded
Show Trash's ⇧⌘T collided with the Show Tab Bar item macOS injects into
multi-window apps. Settled: tabbing is kept as a system affordance (each
tab a full board window, per-board frames applying when standalone) and
Show Trash ships with no default chord, assignable via the remap path.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 21:02:07 -04:00
rzen fdec965700 Give the card window's title field its Escape abandon
The field's exits were commit-only, silently diverging from the board
rename's Escape-abandons rule, and Escape was ambiguous with the body in
Edit mode. Settled: Escape reverts to the on-disk title and focuses the
body; the collision resolves by focus per 06's first-responder routing.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:58:45 -04:00
rzen c145cfb35a Add Restore and Permanently delete to the commit-message vocabulary
The trash trio had no verbs — tombstoning and purging both composed
"Delete card 'X'", erasing the moved-to-trash vs gone-forever distinction
the docs ask users to learn. Settled by diff shape alone: deleted:
appearing = Delete, clearing = Restore, item leaving the tree =
Permanently delete; plural folding keeps Empty Trash distinct from a
multi-select delete.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:57:28 -04:00
rzen 2ee88a6015 Stop ⇧-arrow extension at the trash's liveness and kind boundaries
The rubber-band had its boundary sentence; the keyboard extend gesture had
none. Settled: a ⇧-arrow that would cross live↔trash or card↔lane-entry is
inert — never move-and-replace — keeping the homogeneity invariants
unbreakable by keyboard while plain arrows still walk across.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:56:15 -04:00
rzen fd0f826c3e Tombstoned selections never anchor paste — the ⌘N rule returned to its precedent
Paste's anchor rules never accounted for a tombstoned selection, and a
literal reading filed a pasted lane beside a trashed card's on-disk lane.
Settled: ⌘V stays enabled and behaves as with nothing selected (card
payload to the last-active lane, lane payload to the board's right end).

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:55:22 -04:00
rzen c26a6e3deb Settle search-field keys — stock field, swallowed Return, Tab out, staged Escape
The fixed grammar keys were formally live over the board while the search
field had focus, letting a reflexive Return fire rename/create and destroy
the query. Settled: every key acts on the field (no pass-throughs), Return
no-ops, Tab moves focus to the filtered board with the query intact, and
Escape stages — clear query, then exit field, with board-focus Escape
clearing an active search before its clear-selection meaning.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:54:21 -04:00
rzen 4418b7f981 Align the identity predicate with the ratified shape-only rule
Accept liberally, emit conservatively (764a4d4): the gate is 8-4-4-4-12
hex in any case and any UUID version — uuidgen and UUID().uuidString
print uppercase, and a strict lowercase gate would silently stray an
agent's standard-tool card. Identity comparison is UUID-value equality
everywhere: ItemID keeps its byte-faithful rawValue but equates and
hashes on the lowercased canonical form, and the writer's
import-boundary collision check canonicalizes, so a same-UUID arrival
spelled in another case remints instead of slipping past. The app still
mints only lowercase v4 and never renames to canonicalize.

Full suite 342 tests in 63 suites green. Two findings filed.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
2026-07-26 20:53:38 -04:00
rzen 1d7449e49a Give lane selection its keyboard entry — ⌥↑ escalates; header clicks select
Every lane command keyed off "a selected lane" with no drag-free way to
create one. Settled: ⌥↑ with the lane's first card selected selects the
lane (↓/⌥↓ descend back; ⌥↑ then inert), an empty selection seeds at the
first lane's first card, and a plain click on the lane title bar selects
with the drag surface engaging only on movement.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:45:10 -04:00
rzen 71e7f85328 Registry-cache the welcome row's title and icon with live write-through
The row's name and icon implicitly required reading each recent board's
root index.md at welcome time — the hang the counts rule was written to
avoid. Settled: title/icon/iconColor live in the registry record; they
refresh whenever an open board's reload changes them (in-app renames land
instantly, foreign renames of open boards ride along), with closed-board
renames stale until next open like the counts.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:42:31 -04:00
rzen 747dea552d Surface write failures — banners, locks, one modal
The banner surface as one vocabulary (Kanban/LiveStore/BannerCenter,
Kanban/UI/BannerStripView): a pure precedence rule — in-progress pinned
above the collapse (ratified mid-build), lock > breakage > one-shot
write failures > commit+attachment, signposts last — with all
user-facing phrasing owned here via exhaustive switches over the closed
WriteOperation enum; free-form English survives only in diagnostics.
performWrite posts its failures before rethrowing, so no one-shot can
bypass the strip; refusals under lock post nothing.

The lock vocabulary completes: vanishedRoot and unwritableLocation join
bracketedReloadFailed, each with its own clearing rule (unwritable
clears only on a reconciling reload's writability re-probe). The
registry now owns root recovery: bookmark re-resolution absorbs renames
transparently, a dead root locks read-only and re-arms FSEvents on the
gone path so the root's return round-trips back through rootChanged,
re-minting and re-keying on the way. DirtyBufferGuard is the one modal
moment, retry / save a copy / discard, no fourth button.

36 new tests; full suite 333 tests in 62 suites green. Five findings
filed on the Redesign board.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
2026-07-26 20:41:48 -04:00
rzen dea77c3459 Place Duplicate and Save as Template in the read-only lock's scope
Both whole-board copies were absent from the lock's command enumeration,
staying enabled while the source tree was gone, unknown, or unwritable and
while the lock suppressed their required close flush. Settled: Duplicate
disables in every lock state; Save as Template disables except under the
unwritable-location lock, where reading the board into Application Support
is a legitimate copy-out with a vacuous flush precondition.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:40:40 -04:00
rzen ce1d618117 Rank info rows in the banner precedence — in-progress pinned, signpost last
The stacking order covered only error classes, letting an in-progress row
collapse behind "+N more" — the sole explanation of a bracket's write lock
and a deferred close. Settled: in-progress rows pin above everything,
exempt from and uncounted by the collapse; the passive remote-change
signpost ranks below all error classes and may collapse.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:30:21 -04:00
rzen c3e919be37 Make the trash sort deterministic — tie-break and unknown-date rule
The trash's newest-first order is load-bearing for keyboard input, but
same-second multi-deletes and 01's blessed-unparseable timestamps had no
ordering. Settled: ties break by folder name ascending; date-unknown
entries sort as oldest, after all dated entries; lane entries interleave
by their own stamp.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:28:48 -04:00
rzen 764a4d4a59 Define the identity predicate as shape-only — lowercase v4 is emission, not the gate
01 said "lowercase UUIDv4" without defining the test, and the strict
reading made every uppercase uuidgen/UUID().uuidString folder a silently
skipped stray. Settled: 8-4-4-4-12 hex in any case and any version is an
identity; comparison is UUID-value equality (case-spellings are one
identity, matching default-APFS behavior); the app emits lowercase v4
only and never renames to canonicalize.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:27:07 -04:00
rzen e012147476 Settle launch restoration on a live open-now registry flag
The registry recorded recency but not which boards were open at quit.
Settled: each record carries an open-now flag set on open and cleared on
user-initiated close only — quit teardown leaves it standing, so there is
no at-quit write and crash recovery falls out for free. Restoration
reopens flagged records by lastOpened order; the preference only gates
consulting the set.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:22:07 -04:00
rzen 110ef1e623 Settle vanished-focus recovery when the focused card's lane vanished
10 recovered focus "to the card's lane" even in its own lane-tombstone
case, where that lane renders nowhere. Settled: recovery walks up then
sideways — the lane at the vanished lane's position (next by order, else
previous), board container only when no lanes remain, never into the
hidden trash; the announcement names the lane, not the card.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:20:00 -04:00
rzen 7980bb692b Give Preview checkboxes their pointer-free path via the system focus model
10 promised toggleable-without-pointer, 11's normative inventory had the
toggle as pointer-only, and 02's lock disabled it "via menu validation"
with no menu item to validate. Settled: checkboxes and links are real
controls in the focus/accessibility tree — FKA + Space and VO-Space toggle
them; in-content controls are content, not commands, so 04's menu-item
contract and 11's existence rule scope to commands; under the read-only
lock the controls disable in place.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:18:33 -04:00
rzen c66fae0b09 Trash respects the ancestor walk — no row under a tombstoned lane
03's Put Back cascade ("restores the lane too") was unreachable dead text
under 01's hide-the-subtree consumer rule and 02's effective liveness.
Settled: the lane's single trash entry subsumes everything beneath it; its
count is what Put Back returns to the board; lane Put Back splits by flag
(hidden-with-lane cards return, individually tombstoned ones reappear as
trash rows), making that recovery deliberately two steps.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:16:08 -04:00
rzen c2ff51cf88 Settle within-board lane paste as the supported lane duplicate
04's drag rule called a within-board lane duplicate unsupported while the
clipboard rules quietly permitted it via same-board lane paste. Settled in
favor of the clipboard: the duplicate is supported there (fresh GUIDs,
tombstones stripped, no menu-validation special case) and merely not
available by drag — ⌥ stays ignored on lane drags so the gesture remains a
clean reorder.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:10:42 -04:00
rzen 01ae3fb4b8 Scope modified to index.md rewrites — content provenance, not file activity
01's table promised an update on every app write, which raw-source Apply
(byte-for-byte contract) and attachment operations (never touch index.md;
load-bearing for 03's Add-Attachment-during-raw-source rule) cannot honor.
Settled: modified updates only on app writes that rewrite the item's
index.md; both exceptions stated as deliberate, and the stale-readout
consequence on 05's Modified line accepted with attachment history living
where the files do.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:08:28 -04:00