The editing surface: the same hosted TextKit-1 text view gains an
editable branch with a per-keystroke line-scanner highlighter — chosen
over a parser re-parse because a mid-typing buffer is usually invalid
Markdown and 05 wants the delimiters themselves dimmed; apply only sets
attributes, so presentation-never-transforms is structural. Saves ride
a ~700ms injectable debounce through BoardWriter.writeBody —
toggleTaskMarker's idiom widened to the body span, frontmatter bytes
untouched, refusing to write when disk already holds that body, which
enforces all three gates (untouched, reverted, echo) at the layer that
owns the bytes with one isDirty predicate above it. Mode grammar lands
whole: ⌘E toggles with a checkmark, Return in Preview enters, Escape
returns, and every flip flushes first; window close flushes through
the existing retry/save-copy/discard modal, and the dismissal flush
deliberately reaches a tombstoned card. Dirty-buffer-wins: disk always
follows the snapshot, the buffer only when clean, both surfaces render
the buffer. Undo is the editor's own session-scoped NSUndoManager;
endEditSession names the pro-m1 one-commit-per-session boundary.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
The card body's resting state: swift-markdown (pinned 0.8.0, smart
typography off — Preview renders the bytes on disk) parsed into a pure
BodyMarkup model with UTF-8 source offsets, rendered on one hosted
TextKit 1 NSTextView — chosen because find-in-text is NSTextFinder,
checkbox clicks reuse AppKit character hit-testing, links are .link
attributes, and NSTextTable's automatic layout is exactly the
columns-sized-to-contents rule. The GFM subset renders per 05; HTML
stays verbatim code-styled text; relative images resolve against the
card folder while remote URLs are never fetched, drawing a quiet chip
instead. Task checkboxes are live: a click flips exactly one byte
through a fresh-read, refuse-uneditable, stamp, atomic-replace write —
the app's only offset-addressed write, so a moved target refuses as
staleTarget and what the user saw decides the direction, netting one
toggle on a double-click. Empty bodies open in Edit per CardBodyMode's
opening rule, applied once; the Edit surface itself stays an honest
read-only stub until its card. FindCommand prefers the card body's
find over board search when a card window is focused.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
01's Lanework-owns-the-board carve-out: a regular file beside a card's
index.md belongs in attachments/, and the app moves it there. The
loader detects read-only — a new LoadResult.looseCardFiles channel,
separate from the stray-tolerance warnings because it says the opposite
thing — skipping directories, symlinks, hidden entries, and the
reserved names compared case-insensitively (on APFS, Index.md IS the
index). The relocation rides one performWrite bracket at the tail of
every successful reload, which makes lock deferral free: the reload
that lifts a read-only lock is the reload that relocates. A
lane/card/filename memo keeps a failing relocation from hot-looping —
one one-shot, then silence until disk changes. The notice rides the
loss-row class, phrasing folded by BannerCenter (one file, one card's
files, a multi-card sweep), naming original filenames per the
importAttachment rule. Paste normalizes at the import boundary: staged
snapshots' loose files land in the pasted card's attachments silently,
every arrival path declaring its side via an explicit
normalizingLooseFiles parameter — drag paths decline and fall back to
the destination's own carve-out. checkIsCardFolder closes the hole
where a lane's notes.txt would have been relocated: card depth is
exact, UUID under UUID.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
03's resettlement reverses the pathfinder carry-over: the
selection-keyed dual presentation proved undesirable, so a card has one
presentation — selection changes styling, never geometry, and the
masonry never reflows on click. Deleted the carousel view (page dots,
glass underlay, scroll-tick monitor), the QuickLook thumbnail cache
(sole consumer), the pure paging/suppression rules, and the
sole-selected animation key — Motion now keys transactions on the
search query and the drop proposal only. The attachment chip stays as
the face's whole attachment story; viewing media is the card window's
job. No carousel state had leaked beyond the view layer.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
⌘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
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
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
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
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
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
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
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
WriteOperation replaces free-form operation strings throughout
BoardWriter, per the settled rule in 02 § Write-failure surfacing: the
banner layer will switch exhaustively over it, so a new operation
without a banner rendering is a compile-time hole. Titles enrich at the
two points a document read makes them known (updateIndex, and the
move/copy pre-flight), so failures after the read name the item;
purge never reads and stays title-less. Free-form English survives
only in the diagnostic reason.
Full suite 300 tests in 56 suites green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
importAttachments lands files into a card's attachments/ (created on
first import — the one folder the app ever makes there), never
overwriting and never refusing a name: collisions auto-rename
Finder-style, counting up from 2, with a same-named subfolder
blocking a name exactly like a file. Sources are validated before any
copy; a failed copy removes the partial destination and throws naming
the source file; a multi-file batch stops at the first failure with
earlier files staying landed. listAttachments is the flat surface:
top-level regular files only, Finder-sorted, subfolders and hidden
entries excluded and untouched; a missing attachments/ lists empty.
11 new unit tests; 245 total green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
deleteItem writes deleted: <now> in place through updateIndex — the
folder never moves, nothing beneath it is touched (hiding the subtree
is the renderer's ancestor walk, not a stored flag). restoreItem
removes the key — position-perfect by construction, every duplicate
occurrence taken so a hand-written twin cannot resurrect the
tombstone. purgeItem physically removes the tree; an already-missing
folder is success (a Finder deletion converges on the same end
state), and no prior tombstone is required (Delete Immediately skips
the stage by design). Board-root deletion is structurally unreachable
via the UUID-shape guard shared with move/copy. Neither delete nor
restore polices liveness — re-deleting refreshes the timestamp,
restoring a live item is a harmless stamped rewrite.
14 new unit tests; 234 total green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
moveItem: physical folder move, UUID and created travel unchanged;
exactly one file rewritten (the moved root's order, stamped). Import
boundary detected by resolved board-root comparison; on a cross-board
move-in, arriving UUIDs colliding with any identity in the destination
board (tombstones included) are reminted per folder at the finest
grain — folder rename only, file bytes untouched, every repair
reported in MoveResult. A colliding root moves straight to its minted
name. A same-parent move degrades to a plain reorder, self excluded
from the appended-rank scan.
copyItem: whole-tree copy minting fresh UUIDs at every depth; .fork
keeps created while stamping modified and clearing modified-by, .born
(template instantiation) stamps created fresh too. Nested uneditable
or unreadable files copy byte-verbatim rather than blocking the
gesture; the root must be rewritable. All-or-nothing at the
destination — any failure removes the partial tree.
24 new unit tests; 220 total green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
createBoard fills or mints a .kanban (or extension-less) folder and
refuses to clobber an existing board; createLane/createCard mint fresh
lowercase-UUIDv4 folders and append at max+1024 among visible siblings
via the same strict scan renumber uses (extracted, shared). New files
carry schema/title?/order?/created/modified — created==modified, no
modified-by — written LF through the same atomic temp+rename path.
The editability pre-flight is now scoped to operations that rewrite
siblings: renumber refuses on an uneditable sibling, a create beside
one proceeds. Schema constant unified on BoardLoader.supportedSchema.
Info.plist declares the .kanban document type (UTI conforming to
com.apple.package) so Finder treats a board as one document.
20 new unit tests; 196 total green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
The single point through which every mutation becomes a filesystem
operation: read fresh from disk (strict byte-faithful UTF-8), refuse
readable-but-uneditable frontmatter shapes, apply the edit through the
span engine, stamp modified / clear modified-by, then temp-file+rename
atomically. Renumber-visible-children is the sole minimal-touch
exception, tombstones untouched. Structured BoardWriteError carries
operation + path + reason for the future banner surface.
Alongside, three edges the card surfaced:
- The loader now decodes byte-faithfully too, so a BOM'd or non-UTF-8
index.md is rejected at load per the encoding contract, instead of
loading via NSString's silent BOM strip and then refusing every write.
- An appended frontmatter line adopts the file's prevailing line ending
(a CRLF file stays uniformly CRLF when a stamp first lands in it).
- Empty-but-not-blank frontmatter ({}, null, ~) is detected as
uneditable — appending after it would be unparseable YAML.
30 new unit tests; 176 total green.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
rawValue and the coercion/malformed paths now stop at the quote-aware
inline-comment boundary, so 'title: 2048 # note' coerces to "2048"
and a read-then-write no longer compounds the comment (pinned
idempotent). Strictly read-side; round-trip bytes untouched. +6 tests
(124 total).
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Duplicates: Yams' duplicate-key error is disarmed by placeholder-
renaming first occurrences and re-composing (line count preserved, so
span layout stays exact); reads take the last occurrence, set()
rewrites the winner and clears earlier twins, remove() clears all.
Top-level only — nested duplicates stay YAML errors (doc updated).
Re-splice: an inline comment on a rewritten scalar line survives, with
quote-aware detection so a # inside quotes is never a comment. +31
tests (118 total).
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Only lowercase-hex 8-4-4-4-12 folder names are lane/card candidates;
anything else is a stray — skipped with a distinct warning, never
descended, never able to fail-fast a load. UUID-shaped folders keep the
prior contract (missing index skips, malformed frontmatter fail-fasts).
Design resolution from the Redesign board. +5 tests.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Pure tree walk: root → lanes → cards, level is position. Fail-fast with
path+reason for bad YAML, missing/malformed schema or order, newer
schema, rootless board; index-less folders skip with a collected+logged
warning; strays, hidden files, and symlinks ignored; board-level
deleted ignored with a warning; tombstones loaded and flagged. 17 tests.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Immutable snapshot value types encoding the frontmatter tables: ItemID
wraps the exact folder name (not Foundation UUID, which uppercases);
lanes/cards carry FieldValue-typed fields, display-ordered children
including flagged tombstones, and their full FrontmatterDocument so
unknown and reserved keys ride along uninterpreted. Boards are
identified by rootURL — package folders are human-named, not UUIDs.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
Pure ordering math: append at max+1024, head-insert at min−1024,
midpoint insertion with precision-exhaustion detection (nil on ties and
rounding onto an endpoint), renumbering to whole multiples of 1024, the
shared display-order tie-break (order, then folder name), and
tombstone-excluding overloads. 18 tests.
Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A