d9b6232dea0cbb8960be38b8e0445861a7046e8c
52
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
31fee00c73 |
The decision surface — a refused open becomes a live repair, in place
Phase 3 of the decision surface, completing the card (01 ▸ Malformed input, settled 2026-07-31). An attended open's fail-fast walk transforms the loading window's content into one aggregated surface — never a sheet, never a chain: defects grouped by class, each class stated once with its files listed (Reveal in Finder + Open in Editor per row), a class-level default preselected, per-item override behind a disclosure. Only honest choices: YAML and malformed-schema get Editor + Re-check (Skip below the root); newer-than-app gets Skip alone and blocks the board at the root; the two root repairs — minted index, schema: 1 stamp — are defaults. Repair and Open applies fixes in one store-less write bracket and re-walks: clean proceeds, remainder re-aggregates into the same surface. Cancel and ⌘W retire to welcome's row; restored opens never see the surface at all (OpenOrigin rides the PendingOpen carrier). Skips are per-open consent that rides the session — the store retains the skip set and every reload passes it — and the opened board posts a warning-tone notice naming what was left out, each item's Reveal riding the banner strip's new reveal control. On Pro boards the repair bracket binds its own EchoLedger, heal-marks everything, and the store adopts it before the committer starts, so repairs land as one separate commit authored Lanework Integrity — pinned end to end. Also fixed en route: a retired loading window left its close interception installed and returned false from windowShouldClose forever, blocking quit. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
ba1726fa77 |
The loader collects every fail-fast defect and honors per-open skips
Phase 1 of the decision surface (01 ▸ Malformed input, settled
2026-07-31): BoardLoadFailure aggregates the walk's defects in walk
order — stop-at-first retires. Environmental failures (unreadable root,
not-a-directory) stay immediate single-defect throws: there is no walk
to collect from. A defective root index is recorded and the walk
continues into the children (nothing in the walk consults the parsed
root document — verified); a defective lane, card, or trash-entry index
records and skips its subtree, Re-check's whole-walk re-aggregation
being the designed loop for what hides beneath. load(skipping:) is the
per-open skip channel: a skipped path's item is omitted from the model
and surfaces as LoadWarning.userSkipped; root paths are unskippable by
construction. The reload-breakage banner carries the aggregate ("…and
N more"), single-defect sentences byte-identical to before. Two new
multi-defect fixture boards; suite 2591 green.
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
|
||
|
|
adf4fc5973 |
README: optional below-root keys, the gitignore noise gate, and the true fixture count
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
a7f35a0e5a |
Implement repository hygiene
Add-git seeds a minimal .gitignore (.DS_Store) before the initial stage — the seed rides "Initial board state" and .DS_Store never enters history; an existing .gitignore (or a directory wearing the name) is left alone forever, adoption and repo-nested seed nothing. GitHousekeeping is the periodic loose-object repack: filesystem enumeration of objects/<2hex>/<38hex> (never git_odb_foreach, which would rewrite the whole database into a fresh pack each pass), git_packbuilder_insert one oid at a time, additive pack write — and deletion only after each oid is re-verified against the written pack opened as a standalone one-pack odb with no loose backend. Any failure returns before deleting; the worst case is a stray pack. Nothing prunes, expires, or consolidates — existing packs accumulate, recorded as the accepted cost of never rewriting storage the app didn't write. GitHousekeeper schedules it: git's own 6700 threshold, 8s after session activation (outlasting the launch catch-up), background priority, skipped under pause states, held locks, or an in-flight commit, never retried — the next open tries again. Free tier composes none of it. 20 new tests: full-odb equality, per-oid survival, identical walks, byte+mtime-identical refs/HEAD/working tree, whole-.git identity on every declined pass, and deleting-never-forgets. 2394 tests / 412 suites green; InertGitTests untouched. Closes pro-m1-git-undo. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
1f7d84bf64 |
Build branch switching and the popover git surface
GitBranchSwitcher holds 06's sequence as one object: settle editors
explicitly (SessionSettleGate — Save All applies raw buffers with
validation and a refused buffer cancels the whole switch; Discard
reverts buffers AND reconciles the session folders against HEAD;
never silent), flush the pending auto-commit, stamp intent in the
per-board registry, bracketed safe checkout (git_checkout_tree
GIT_CHECKOUT_SAFE + set_head — no path passes FORCE, abort
included), one reload via the async wholesale bracket (failed final
reload engages the existing read-only lock), reseed undo/redo from
the new HEAD with redo empty, clear the stamp. Create-and-switch
keeps the full sequence — the tree-cannot-change proof fails under
concurrent writers. Lock contention shows the 02 in-progress row's
waiting state ("waiting for another writer's git lock"), bounded at
30s then failing cleanly naming the lock path.
GitOperationStamp + GitOperationRecovery: the own-leftovers rule as
a pure conjunction — pause state AND matching stamp = the app's own
interrupted operation, aborted to the pre-operation state with a
banner, stamp cleared on success only; either alone defers to the
pause-and-defer stance. Checked where the committer starts.
BoardGitControls replaces the read-only branch line: branch picker,
inline create-and-switch, the abnormal-state pause note in 06's own
words with controls dimmed, and commit-identity fields that read and
write repo-local .git/config (derived default as placeholder, never
value; unfocused resync, focused keystrokes kept; 2s poll while
visible — .git is watcher-filtered by design).
Also fixes a shipped bug from the undo card: plan(reconciling:)
matched card ids as path prefixes, so the reconcile branch was inert
on every board (<lane>/<card> never matches a bare id) — a session
file the restore diff couldn't name (attachment, comment, draft)
survived Discard and landed in the next flush's commit. One shared
component-exact folder-name resolver now serves both Discard paths;
noteDiscarded takes cardFolderName; regression test verified failing
against the pre-fix code.
41 branch tests + the regression; 2374 tests / 409 suites green;
InertGitTests untouched.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
|
||
|
|
142c6e75fe |
Implement undo and redo as forward commits
GitHistoryProvider is the second HistoryProviding implementation: its stack IS HEAD's first-parent ancestry, reseeded on load (redo empty), re-synced to HEAD before every crossing so agents' self-commits become the top and ⌘Z steps back exactly one commit; any arriving commit clears redo (a heal-only window deliberately does not). Restores are forward commits through the ordinary signature path — GitRestoreOperation materializes only the current-vs-target diff as working-tree writes and resolves no reset/checkout symbol at all; heal commits are transparent in-session (pointer passes over, restores exclude heal-owned paths, identity carried on landed windows via PlannedCommit.kind → GitLandedCommit). Subjects "Undo:/Redo: <crossed subject>"; menu labels never nest in-session; the root commit is not a step (crossing it would restore the empty tree). Provider binding flips: makeHistoryProvider(store, tier, git) — free binds native everywhere, Pro binds the git provider on git boards and NOTHING on mode-none/repo-nested (the pair disables through existing validation); add-git mid-session live-binds via HistoryStore.didAddGit → bindHistoryProvider (the flip only ever adds). SessionSettleGate is the reusable Save All / Discard / Cancel step: restores whose diff touches an open Edit session or raw-source buffer gate on it (Save All applies with validation — a refused buffer cancels the whole restore focused on the offender; Discard reverts via CardBodyEditSession.discardBuffer and reconciles against the working tree, deliberately skipping the second flush); untouched sessions ride through undisturbed. Built for the branch-switch card to reuse. BoardStore gains the async performWholesale sibling. CardHistorySection fills the m6 EmptyView slot: read-only, newest first, follows the card across lane moves by folder-component match (the UUID is the identity — no rename detection), absent off git mode and off Pro. 2332 tests / 403 suites green; InertGitTests untouched. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
563999655f |
Build the semantic commit-message engine
CommitMessageEngine replaces the interim composer as the wired default: a pure total function from two snapshots + changed paths to a message. Full vocabulary — Add / Delete / Move / Rename / Edit / Restyle / Resize / Reorder over cards, lanes, board; Attach / Remove; Repair for the duplicate remint (detected as a heal-classed rename-paired arrival whose id the previous snapshot never held — the loader withholds duplicates, so the shape is a bare arrival); the trash triple by diff shape alone (into .trash = Delete, out = Restore, leaving the tree = Permanently delete); Relabel / Assign / Set due date plus the named generic for custom keys. Plural folding with shared destinations, implied events as body bullets never subjects, ~40-char subject truncation, "(untitled)". Bookkeeping (sequence-preserving renumbers, stamps, backfilled kind) composes nothing. Non-snapshot paths compose path-shaped events — CLAUDE.md reads "Update agent guide (vN)" via the marker line (the m10 card's deferred bullet lands here), everything else "Update '<path>'". The comment verb family per 01's ruling (comments shipped, so 06 gains the verbs): Comment on / Edit comment on / Delete comment on / Draft comment on / Permanently delete comment on '<card>', grouped one event per comment folder, classified ahead of the model-silence rules, card title resolved from either snapshot. GIT_DELTA_ADDED is surfaced as GitChangedPath.isArrival — post vs edit is unanswerable from snapshots that exclude comments by ruling. A card moving with its thread swallows the comment events (implied-events one level down). The previous snapshot is HEAD's tree, materialized per flush into a temp dir (index.md blobs in full, other blobs zero-byte — the model reads attachment names, never bytes) and re-parsed through the one BoardLoader; never a value carried forward. changedPaths is a hard filter per split commit, which also earns the stage-around and kills phantom events. Launch catch-up and foreign windows compose through the same engine. 48 new tests (35 pure + comment family + engine-level); 2293 tests / 394 suites green; InertGitTests untouched. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
3c07c26fda |
Build the auto-commit engine
Every settled change on a git-mode board commits, debounced 2s past drag/typing churn, staged whole-root with .gitignore respected. GitCommitOperation reaches the vendored libgit2 directly (same 1.9.2 pin SwiftGitX resolves — importable, not duplicated) for signature-capable commits; add-git's config materialization is gone, identity resolves at commit time (repo-local config, else derived default) per the 2026-07-31 ruling in 06. CommitAttribution classifies per file off EchoLedger receipts: user identity on app-mediated windows, Lanework External <[email protected]> on foreign, the modified-by refinement (<slug>@agents.lanework .invalid) when every foreign file agrees, heal-marked receipts split into their own commit — window split foreign → heal → user. Edit-session granularity: ~700ms saves stay uncommitted, staging excludes open session folders (closure-resolved so mid-session moves stage around the new location), session end nudges the debounce so each session lands exactly one body commit. Flush-before-overwrite gates on known-foreign windows and commits synchronously ahead of the write; close/quit flush the pipeline via CloseFlushCoordinator's committerFlush. index.lock backs off briefly then re-debounces silently; clean tree no-ops; genuine failures ride the standing history-suspension banner and retry next debounce. Abnormal repo states (detached HEAD, merge/rebase/cherry-pick in progress) hold the engine with a 15s re-check; unborn HEAD commits "Initial board state" whole-tree; dirty tree at open catches up through the same engine. Message seam (CommitMessageComposing) ships interim — the semantic composer is the next card. Discovery diffs HEAD against an in-memory index with rename detection (git status alone never pairs a bare mv), and a failed survey reads as "could not look", never "nothing changed". 46 new tests / 8 suites, all real repositories via bundled libgit2. 2240 tests / 383 suites green; InertGitTests untouched. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
189af238a1 |
Build HistoryStore — opt-in git and mode detection
The pro-m1 foundation card. SwiftGitX 0.4.0 (bundled libgit2, the pathfinder's pin) joins the one target; new Kanban/Git/ holds BoardGitMode (pure nearest-.git-wins detection, .git-as-file counts, NSString ancestor walk), HistoryStore (@MainActor @Observable; compose() is the tier gate — free tier gets no object, no detection, no stat), GitRepository (scope-confined SwiftGitX handles: create = init + HEAD forced to main + whole-tree "Initial board state" commit; branch reads incl. unborn/detached; path-history ranks), GitIdentity (derived default as a pure function + repo-local config reader — not libgit2's merged ladder), and GitPathHistory (Mutex-guarded lazy ranker). beginSession composes the git state beside the tier and feeds BoardStore.makeIdentityHistoryRanker; git-mode loads pass the git-backed IdentityHistoryRanker to BoardLoader. The popover's git slot resolves a pure five-way matrix: free tier unchanged (absent / BoardGitNote), Pro mode-aware — Add Git on mode none, honest prose on repo-nested, read-only branch line on git. Provider binding unchanged: both tiers still bind native until the undo/redo card. 42 new tests across 8 suites, all repositories built through bundled libgit2; InertGitTests untouched and green. 2194 tests / 375 suites. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
fe3ffac48e |
Comments, phase 2 — the pane, the composer, and the inline session
The card window recomposes into three componentized panes (body, comments, attributes) with two mounts — beside or body-over-comments at ~3:2 — behind View ▸ Comments Beside Body. View ▸ Show Comments is one persisted app-wide bit, no content-derived auto-show; File ▸ Add Comment flips it on and focuses the composer. The thread renders author lines, edited markers, card-subset Markdown bodies, and read-only Quick Look chips under a count header with the sort- direction control. The composer edits comments/.draft/ on the slow cadence (blur, close, quit, ~30s interval), Escape only moves focus, ⌘↩ posts. Inline edit is a body-edit session in miniature: 700ms debounce, Save/⌘↩ commits, Cancel and Escape revert to session-start bytes, close flushes. File drops within either authoring surface carve out of the window-wide card default into that surface's attachments/; paperclips cover the no-drag path. Close flush runs inline flush, then draft save, then the comments/.trash purge; open sweeps crash residue. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
3b19883593 |
Lanework Pro is a subscription — the tier seam, StoreKit 2, and Settings
Phase 3 of the one-app pivot (DESIGN 12 ▸ The entitlement / Distribution, ruled 2026-07-30; card c3a3ddd5). New Kanban/Tier/: Tier (.free/.pro — deliberately no .lapsed case; unsubscribed and lapsed are one state) and the pure decision Tier.resolve(from:now:) over SubscriptionFacts (expiration + willAutoRenew), unit-tested through all five named states: free, active, lapsed, offline-grace, never-online. The facts are a persisted cache (standard defaults), not a live view: StoreKit ages an expired subscription out of currentEntitlements locally, so an offline device and a real lapse are indistinguishable from that property alone — the cache holds the last answer, empty entitlements read as silence, and holds end only on a definitive answer (revocation, or the subscription-group status read Settings performs). That is 12's offline-grace trade, resolved toward the paying user. ProEntitlement is the local adapter (currentEntitlements + Transaction.updates, started from launch, never from a test host); ProStorefront holds everything networked (product load, purchase, AppStore.sync) and only the Settings section ever constructs one — the split is the enforcement of "never network on the open path". beginSession reads the tier once at composition; BoardSession.tier is a let with no path back in, so a lapse never rebinds an open session. makeHistoryProvider now takes the tier; both tiers bind the native stack until pro-m1 builds the git provider — the seam's consumer is named, not invented early. Settings gains the Pro section (subscribe with localized price, manage, restore; a quiet unreachable line, no indefinite spinner) — the third of the exactly-three Pro mentions; the About line gains its "…in Settings" pointer now that there is a Settings to point at. A successful purchase or restore offers once to reopen open boards (close + reopen through the ordinary paths). Configuration.storekit wired into the scheme's run action for ASC-free exercise; RELEASE.md gains the pro-m1 store-side steps and the rule that the product must not be configured before then. 1901 tests in 319 suites green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
092300c7d2 |
Collapse the edition split — one target, twins merged, verify-editions retired
Phase 1 of the 2026-07-30 one-app pivot (DESIGN
|
||
|
|
f7c8088783 |
Lanes delete into the trash — rendering, grammar, drag, clipboard, a11y
Phase 2 completes the lanes-in-trash card. TrashEntry merges the trash's two kinds by rank in exactly ONE place (ItemPath.resolve's own merge deleted in favor of it — the three-merge-points finding shrinks instead of growing). TrashLaneRowView renders the opaque row — tertiary plate, level-default lane glyph never the lane's own icon, title + card count, no accents, no expansion; the column badge counts rendered rows. Selection grammar: kind-homogeneous trash selections — ranges skip the other kind, ⇧-extension stops at the kind boundary, plain arrows walk the merged order, marquee stays card-only (now load-bearing: rows register frames for arrows), Select All card-scoped; successor-on-purge crosses kinds like navigation as the interim for open Gap 7b5cbc90. Drag: TrashDrop accepts lane sessions (drop on shown trash deletes), restoreLanes routes a trash-sourced strip drop as an arrival-ranked within-board move with an undo step. Clipboard: ⌘X/⌘V lane restore via opaque lane subjects; fixed boardRoot(ofLaneFolder:) returning .trash as the root — a same-board restore looked like an import and would have reminted the lane it was restoring (pinned by test). A11y: row = one flattened "title, deleted lane, N cards" element with Delete/Reveal actions; BoardDiff crossings read lanes as deleted/restored, shown-trash churn digested at row level. Agent guide stays v7 — the literal already teaches lanes-trash-by-move and kind stamping; drift-guard pins those lines. README trash paragraph notes lanes. Both schemes 1893 tests / 322 suites green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
785ef5fe14 |
Realign code with the 2026-07-30 findings-resolution rulings
Delete Immediately is removed entirely (ruling |
||
|
|
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,
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
c339b4cecf |
Implement live accessibility announcements
The board speaks when files change under the user, per DESIGN/10 § Live board announcements. BoardDiff is the pure snapshot summarizer (identity sets for cards/lanes added/edited/moved/deleted — ids, not tallies, so pro-m1's semantic commit engine can build on it; edited = rendered content only, moved beats edited, implied events don't steal the subject). BoardAnnouncer is the decision seam: focusOutcome computes the vanishing-focus sentence and the walk-up-then-sideways recovery (next lane by order, else previous, board container only when none remain, never the trash); speech(for:) is the one-sentence precedence ladder — raised condition > bracket completion > cleared condition > vanished focus > digest — foreign-only for the last two rungs, so app-mediated echoes stay silent. BoardStore.land assembles ReloadFacts and posts exactly one sentence per reload through the injectable announce outlet (AccessibilityAnnouncer, medium priority, never interrupting). Selection recovery layers on top of ItemReferenceSet re-resolution — survivors veto, the emptied selection lands on the vanished item's lane and re-arms ⌘N's active-lane memory. performWholesale(announcing:) arms a completion phrase consumed by the closing reload — nil on every base bracket today; pro-m1 fills git phrasings. Locks raised outside the reload path (vanished root, unwritable location) announce through the same ladder, and the banner strip is a labeled "Board status" container whose row labels are the announced sentences (AccessibilityPhrases.bannerLabel — one string for eye and ear). Announcements classify at reload granularity (WatchOrigin) as a deliberate interim: DESIGN/02's EchoLedger (per-file classification, the announcer's specified input, git-free) was scheduled with the auto-committer that the edition split moved to pro-m1 — filed on the Redesign board for a ruling. 1533 unit tests green, both schemes build. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
b3812ed928 |
Build AgentGuide — versioned CLAUDE.md maintenance
The app-owned agent guide at every board root (DESIGN/08 ▸ The agent guide): version-gated by a first-line marker (v5, superseding the pathfinder's v4 guides on real boards), rewritten when missing or older, byte-for-byte untouched when current or newer. A markerless CLAUDE.md is displaced to CLAUDE.user.md when that name is free — never clobbered — and the guide write is skipped with a log when it isn't. Symlinks, folders, and read-only volumes are skipped in silence; the write rides performWrite's bracket as an app-mediated Writer operation (new WriteOperation.agentGuide), so the echo lands appMediated and the Pro-era committer can attribute it honestly later. Hooked at store acquire (beside the loose-file relocation, after the watcher exists) and on every successful reload — the guide self-heals from foreign deletion or rollback, pre-wiring 06's acknowledged undo bounce. The refresh memo arms before each attempt and clears on a successful write, so a failing write can't hot-loop and a foreign deletion stays healable. First-line-only marker parsing (no Regex); guide content is one swappable literal, finalized under the next card. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
3aa80db2a4 |
Build the template chooser and Save as Template
The chooser completes its three tiers: bundled by template order, then keyed user templates, then keyless boards by display name — and a malformed user template still lists, by folder name with the loader's own sentence on the row, never failing its neighbours. The store is re-scanned on every presentation and on app activation, the Reveal round trip made honest without watching a folder 09 deliberately leaves unwatched; Reveal lives in the chooser's header and mints the store on first press. Save as Template repeats Duplicate's sequence — progress row with Cancel, flush, detached cancellable copy — through the engine: mint the store, read the next user order before the copy can count itself, Finder-ladder the name, copy excluding .git and .trash/, then stamp the whole template: mapping on the landed copy through updateIndex, with no bracket because the copy lives outside every watched board. Folder attributes deliberately don't carry — the one lock the command stays live under is the read-only-DMG one, and carrying its mode bits would mint a read-only template in the user's own store; the command gates instead on the real hazard, unsaved card content. A signpost names the template only when the ladder renamed it. One name ladder now serves Duplicate and the store. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
b6f559375b |
Build the template engine — board-as-template instantiation
A template is a board folder the ordinary loader reads — no second schema, no Swift catalog. BoardTemplate became exactly that: a loaded BoardModel with chooser-facing derivations, the lane-title stub gone. TemplateEngine instantiates by the copy-remint-restamp walk: .git and .trash excluded at top level only — both names mean something at a board root and nowhere else, and .gitignore must survive — every materialized folder reminted, created/modified stamped fresh (born today, not forked), modified-by cleared, the template: key carried inert, the blurb and style inherited, and loose card files normalized at this import boundary per the paste precedent so a new board never opens with a notice about a mess its own birth made. Legacy deleted: keys copy through verbatim to the one migrator — stripping would resurrect, skipping would destroy. Atomicity is construct-then-clean: a sibling temp can be sandbox-refused and a cross-volume rename is just a second copy, so the call removes what it created on every non-board exit and never touches an occupied destination. The cancellable per-item walk extracted into BoardTreeCopy serves Duplicate and instantiation with two parameters — top-level exclusions and folder-attribute carriage, the only axes they differ on. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
797d020d01 |
Materialize the trash — faces, menus, and grammar
Phase 3 finishes the pivot at the surface. One card face serves two containers: CardFaceView extracted with a role — board or trash — so stripe, tint, chip, selection stroke, cut dim, marquee registration, and drag are shared by construction, the trash side differing only in its absences: no Open, no rename, no Style, no file-hover highlight, and a Delete that goes through the confirmation host. The column rewrote around the lanes' own single-column masonry so drag reflow reads as positional slides; chrome stays the hatched header, symbol, and count — 11 gives Empty Trash to the File menu alone. Two real grammar bugs die here: plain Backspace on a trash selection purged without the confirmation the menu raises, and the context menu's Delete resolved against the standing selection, so right-clicking a trash card under a board selection silently did nothing — it now stages the clicked set explicitly. Open, Rename, Style, and Empty Trash validation became testable store seams; the column is one named accessibility container of ordinary card elements. The tombstone era is swept: deleteItem, restoreItem, stripTombstonedChildren — dead since lane copies stopped nesting trash — the restore verb, the unreachable put-back banner row, and every quasi-lane doc comment. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
96c4014fef |
Wire native undo into menus, toolbar, and command validation
The command surface was already almost entirely platform machinery — this card proves it and pins it. Headless probes established that NSWindow.validateMenuItem answers enablement AND rewrites the row title from the delegate-supplied manager, so 'Undo Move 3 Cards' flows step phrase to Edit menu with no code of ours; under the lock the rows dim and keep their names, the correct reading of the-stack-survives. The toolbar twins validate through validateUserInterfaceItem, which never touches labels — 03's static-label exception proven rather than asserted — and their specs' enablement abstention is pinned so nobody later adds a second, disagreeing answer. The one link a headless run cannot close is the nil-target key-window resolution itself: standard responder-chain behavior with none of our code in it, left as the manual check. Base-edition 'disabled without undo' scaffolding is reworded away — every base board has undo now. New suites cover the trash's two doors (delete-then-undo byte-identical to Put Back's effect), position-preserving restore of a middle card, and the capstone: five gestures forward, five presses back to the origin board, five forward again, the menu phrase asserted after every press. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
d61ce422a3 |
Give the popover git slot the contextual Pro posture
12's settled ruling supersedes the m4 placeholder: on an ordinary board the git section is simply absent — the popover is complete in itself — and on a board carrying an inert .git it shows only the calm one-line note, 'This board has a git history. Lanework Pro works with it.' The detection is a pure one-line seam checking the board root at popover open, deliberately non-live: .git is filtered from the watch by design, so there is no reload to hang a live fact off, and a quiet signpost self-corrects on next open. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
f37892c9a9 |
Split the project into Lanework and Lanework Pro targets
Two app targets from one source tree — no build flags, no #if in shared code: an edition difference is a file one target compiles and the other does not. Base keeps everything it had (dev.rzen.indie.Kanban, minimal entitlements, AppIcon); KanbanPro compiles the same sources plus the reserved KanbanPro/ root (Git/, Remote/, Auth/ land with pro-m1 — libgit2 deliberately not added yet), adds network-client and its keychain group, and hand-writes its Info.plist with the UTI block verbatim — base exports the type, Pro imports it, one format either app opens. The unit-test sources compile twice, once per host, with Pro's module aliased so 56 test files keep @testable import Kanban unchanged; scheme Kanban stays the muscle-memory command and LaneworkPro joins it. InertGitTests pins the base posture with bytes and mtimes — a full editing session over boards carrying realistic .git trees at root and nested in a card leaves all twelve entries untouched, and moves and copies carry them verbatim. scripts/verify-editions.sh proves the rest: 26 checks over signatures, symbols, entitlements, identity, and the shared UTI, discounting Xcode's test-host exceptions by name rather than silently. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
06ee59e24b |
Implement toolbar customization for both windows
NSToolbar through the existing HostedWindowController rather than SwiftUI's toolbar — for reasons that are contract, not taste: 03's transient-search clause is a decision over the toolbar's current contents, which NSToolbar publishes and SwiftUI's API cannot answer; Undo/Redo are the system's nil-target responder-chain actions so the toolbar items validate exactly as the menu rows do (disabled on base boards, alive in m8 unchanged); and the search item hosts the real NSSearchField with explicit first-responder control. Customization is all system furniture — Customize sheet, drag rearrange, display-mode popup, overflow, autosaved per window kind. Board default: the search field alone, trailing; catalog adds New Card, New Lane, Undo, Redo, Show Trash, every action extracted from its menu command so no second predicate exists. Card default: the Edit Body / Raw Source toggles and Add Attachment, mirroring their commands' own predicates live via observation tracking. Removing the search item keeps the promise — ⌘F surfaces the same field as a transient strip under the title bar, persisting until the query clears, and an overflowed item that cannot take the keyboard falls through to the strip too. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
46397c740e |
Build the attachments sidebar section
The card's complete file inventory: compact QuickLook-thumbnail rows over Card.attachments — no reference tracking, subfolders tolerated and unsurfaced — with a quiet header add affordance and the drop hint empty state. The whole window is the file-drop surface, Edit mode included (the editor's drag types were already filtered; now tested), sharing the board's folder-refusal semantics literally: FinderDrop moved verbatim into its own file so both windows run the same partition and loss row. Dragged text still lands at the caret and is inert elsewhere — the window delegate accepts file payloads only. Rows open on double-click or Return, drag out their file URL, and Remove is a bracketed write through FileManager.trashItem — the system Trash, never a hard delete, returning the in-Trash URL so the promise is testable; the attachment listing is the guard, so traversal and subfolder names refuse in one line. Keyboard-native per 05: the section is one Tab stop, arrows walk rows by name, Space toggles the shared QuickLook panel, Backspace removes. File > Add Attachment (shift-cmd-A) comes alive through the same import path. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
40c0a75c24 |
Build the Raw Source outlet
The escape hatch: View > Raw Source (opt-cmd-E) unmounts the whole content area for the literal on-disk index.md in a plain monospaced editor with Cancel/Apply. Raw source is window-level state, not a third body mode — entry rides setMode(.preview), which flushes the Edit session by construction, then reads the file fresh; exit reveals Preview, and an empty body after Apply does not reopen Edit (openIfNeeded already ran). Apply validates the proposed bytes through the loader's own card checks — parseDocument's strict UTF-8/BOM rejection, schema, order — deliberately skipping the uneditable-shape refusal, since a flow-mapping card is exactly what the hatch repairs; invalid bytes alert in place with the loader's own error and no bracket opens. The write is byte-for-byte with no modified stamp and no modified-by clear, per 01's explicit carve-out — the verbatim contract outranks stamping — and identical bytes write nothing. Escape cancels, cmd-Return applies, toggle-off applies too, and cmd-E disables while raw is active via a testable predicate. Tombstoned targets refuse as vanished: a foreign delete is never reverted by a stale buffer. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
e989c1f26e |
Build Edit mode with debounced, byte-honest saves
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 |
||
|
|
1e65b7c986 |
Build the card window shell and lifecycle
The m4 scene plumbing was already honest — one WindowGroup value per CardWindowRef enforces one-window-per-card, and CardWindowFate's ancestor walk answered dismissal — so this card fills the window: a two-column shell whose body column takes all resize flex and whose sidebar width derives once from font metrics (26 characters of average body advance plus em gutters), the five 05-ordered section headers as placeholders, and the card body as selectable plain text until Preview mode lands. The fate walk now returns a CardPlacement (card + lane), so one pass answers both liveness and the live board › lane subtitle; a board rename lands for free through displayName. Card windows remember their frames per card in the board record (case-folded id keys, unchanged-writes-nothing), restoring instead of cascading; only unremembered cards take the last-used size and cascade. Store acquisition stays gated on liveStore — a card window never opens a board — and the close-flush hook stands with nothing to flush until the Edit-session card. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
af1860debf |
Relocate loose card files into attachments
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 |
||
|
|
1020d9fca4 |
Remove the face carousel — one card presentation
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 |
||
|
|
2fc4020a2b |
Build the sole-selection attachment carousel
The card face's in-place expansion (03-board-ui.md § Card face): a card with attachments, as the sole selection, grows a paged media band below its title; every other state stays compact behind the quiet paperclip. - CardCarousel owns the pure rules: the sole-selection predicate decides by identity (a sole-selected lane matches no face, no snapshot walk), and expansion is suppressed outside the animation key while a rubber band is active — a band names a set in progress, so carousels neither flicker nor animate under it. - QuickLook thumbnails generate off-main into a per-window cache keyed to survive reselection, with the Finder-icon fallback while loading and for non-previewable types; pages ride the platform paging behavior, dots (glass underlay, solid under Reduce Transparency) click to page, and a local wheel monitor turns a discrete tick into one clamped page — precise trackpad pans fall through untouched. - The expansion animates under Motion's new carouselExpansion transaction keyed narrowly on the sole-selected card; Reduce Motion goes instant. Drop-slot math and the marquee read the expanded height for free — both re-register on every size change. 928 unit tests (24 new). m5-interactions complete. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
328cfb629f |
Write the write-path fidelity test suite
The executable spec for BoardWriter's cross-cutting guarantees, in a dedicated WriteFidelityTests.swift: minimal-touch verified down to sibling mtimes across a seven-operation session; renumber pinned as the sole exception (whole lane rewritten, board root / other lanes / tombstones untouched); delete→restore byte-precise (only the modified line differs, zero tombstone residue); unknown-key order preserved with keys interleaved among schema-owned ones; and a composite building a board purely through the writer, hand-editing a file like an agent would, then moving/copying/deleting/renumbering to a zero-warning reload. Guarantees already pinned by the per-operation suites (atomicity, move/copy identity, body round-trip) are referenced, not duplicated. Shared fixtures promoted to WriterTestSupport.swift. README gains the write-side feature bullet. 5 new tests; 250 total green. Closes milestone m2-storage-write. Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A |