Commit Graph
208 Commits
Author SHA1 Message Date
rzen 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
2026-07-30 18:14:58 -04:00
rzen 2c6b8fe63a Remove the App Group wholesale — one sandbox, one bookmark, one flag
Phase 2 of the one-app pivot (DESIGN 12 ▸ App-side state, re-ruled
2026-07-30; reworks 566deab). AppGroup retires; what remains is
AppStateHome — ordinary sandbox Application Support as the one home for
the registry, clipboard staging and template stores, keeping the
unit-test-host redirect (the test host is the app and would sweep real
state). Scalar defaults return to UserDefaults.standard.

BoardRecord's per-edition grant slots and openNow flags collapse to one
bookmark + one isOpenNow; the legacy-key decode and adopt-in-memory
paths go (nothing shipped with group-era records), while the founding
four-keys-required / defaults-for-everything-since decode policy stays —
a bookmarkless record decodes as the born-orphan row rather than
quarantining the list. needsReopen and the pre-anchored re-grant panel
are removed whole: the only state that flow served — a record granted by
a sibling sandbox — is unrepresentable now, and a dead bookmark of our
own was already the orphan case by explicit comment. The
indexOfRecord path fallback dies with it; path is never a key again.

The cross-process freshness stamp (mtime+size re-read) and
BoardEditionPresence with its popover "Also open in…" line retire; the
clipboard prune keeps its atomic .sweeping/ claim-then-delete, reframed
for crash residue and open -n copies rather than sibling editions. The
application-groups entitlement key is gone.

1880 tests in 317 suites green (13 cross-edition tests retired with
their subject).

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-30 17:46:32 -04:00
rzen 092300c7d2 Collapse the edition split — one target, twins merged, verify-editions retired
Phase 1 of the 2026-07-30 one-app pivot (DESIGN 0bec9a6, card c3a3ddd5):
the KanbanPro target, LaneworkPro scheme, KanbanProTests module-alias
bundle, KanbanPro/ source root and scripts/verify-editions.sh retire
wholesale. project.yml reads as a single-target file again (anchors
inlined, header rewritten in tier vocabulary).

The edition twins merge: EditionTypes -> PasteboardTypes (one
UTType(exportedAs:) home — the one app owns the family types),
EditionAbout -> AboutBox (the quiet Pro signpost survives as the About
box's one line; "…in Settings" deferred until the StoreKit phase gives
it somewhere to point). InertGitTests drops its Base prefix — the
inert-.git posture is unconditional app behavior, unsubscribed and
lapsed being one state.

Entitlements gain com.apple.security.network.client, declared now and
dormant until Pro's remotes use it; no keychain access group. The App
Group key deliberately stays — it goes with AppGroup.swift in phase 2,
since pulling it first would silently drop the app into the fallback
container. README/RELEASE.md build-and-pipeline prose updated to the
one-record world; the subscription story lands with phase 3.

1893 tests in 322 suites green.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-30 17:25:50 -04:00
rzen 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
2026-07-30 17:04:30 -04:00
rzen 8014bde7c6 Lanes delete into the trash — storage, loader, writer, and undo
Phase 1 of the lanes-in-trash card (2026-07-29 ruling, docs led the
code): lane delete is a move into .trash/ with the subtree intact,
arriving at top trash rank — no destructive delete remains outside
the trash.

TrashedLane opaque unit (id/schema/title/order/heldCards) beside
trash cards — deliberately not a Lane, so no card-shaped surface can
believe an empty subtree. Loader's trash walk trusts the kind VALUE
(lane → opaque unit w/ held-card count counted at the loader's own
unit; card → ordinary card; absent/unrecognized → UUID-children
shape, empty-kindless falls to card per 01's honest limit). Writer:
moveIntoTrash generalized with kind passed never derived (an empty
lane would re-derive as card), deleteLaneToTrash mints against the
whole-container rank ladder. Retired: migrateTombstonedLane (lane
deleted: now ignored — loads live, bytes inert, tolerate-tier
warning), removeLane, captureSubtree/recreateSubtree and the
subtree-snapshot machinery. Undo inverse = move back to captured
strip position, redo replays at captured trash rank. Purge walks
lane subtrees; TrashModel.Freight phrases confirms with lane freight
("…and its 5 cards"). ItemPath gains .trashLane; resolve interleaves
the trash by rank; SearchFilter matches lane rows by title only.
Trashed-lane card windows dismiss and pending cuts void via the
ordinary vanish rule — no new plumbing.

Phase 2 (rendering, selection grammar, drag, a11y, agent guide)
follows. Both schemes 1858 tests / 318 suites green.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-30 16:16:57 -04:00
rzen 785ef5fe14 Realign code with the 2026-07-30 findings-resolution rulings
Delete Immediately is removed entirely (ruling ae1dd96, Redesign card
d40bfac1): the delete vocabulary is purely staged — board → trash,
trash → permanent (confirmed on no-git boards), Empty Trash for bulk.
Gone: File ▸ Delete Immediately (⌥⌘⌫) and its validation, both
⌥-alternate context rows (card + the permanently-disabled lane row),
the VO custom action, BoardStore.deleteImmediately,
TrashModel.canDeleteImmediately, TrashConfirmations' .purge action
(zero surviving callers — trash-side Delete always used
.deleteTrashCards), and the pinning tests. purgePrompt drops its
now-single-purpose container parameter (.trash is the only surviving
caller). BoardWriter.purgeItem survives — create-undo rollback still
needs it — with its comment rewritten. README's trash paragraph drops
the ⌥⌘⌫ sentence.

The other 2026-07-30 rulings (2ec2c95 registry freshness stamp,
c741b02 unified-log-as-coerce-consumer) required no code changes —
already conformant.

Both schemes 1844 tests / 318 suites green.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-30 15:13:31 -04:00
rzen 9ca8ed84de 08's guide description goes present-tense — per-version changelog retired
2026-07-30 findings-resolution ruling (Redesign card c70d7a72): the
guide-contents section now states what the shipped guide teaches NOW,
normative against the literal, edited in the same change as the literal.
The hand-maintained per-version bullets drifted within days of v6
(claiming kind and lane-trash teachings the shipped body lacked) — the
README deferred-list failure class; version history lives in git.

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

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

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

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

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

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

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

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

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

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

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

Both suites green, verify-editions 30/30.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Board: Minor card c9c4de2f → Resolved.

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

Board: Minor card cb3bc015 → Resolved.

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

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

Board: Minor card a69e5985 → Resolved.

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

Board: Minor card 7e43562e → Resolved.

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

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

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

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

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

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

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

Board: Gap card 4d95f269 → Resolved.

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

Board: Gap card 64a1c409 → Resolved.

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

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

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

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

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

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

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

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

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

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

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

69 tests added. 1738 green on both schemes.

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

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

1669 green on both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 14:32:31 -04:00
rzen 62c47a2209 Claimed-name squatters heal by displacement — Lanework owns the board
User ruling on the squatted-.trash gap: a wrong-typed node on an app-claimed
name (a file or symlink squatting .trash, a directory or symlink squatting
CLAUDE.md) is an invalid artifact, not a resident — a scheduled heal moves
it aside via the Finder-style rename ladder, preserved verbatim, with the
relocation-style notice. Upgrades the agent guide's untouchable-skip to
displace-then-write; the CLAUDE.user.md-taken skip stands (destinations
don't cascade-displace). Until the heal lands the loader keeps the
empty-trash read and a racing delete fails as a one-shot — a one-reload
window, not a standing state.

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

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

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-29 14:05:16 -04:00
rzen 1b02f6e063 Formalize the integrity taxonomy and service — the validator design pass
01 gains Validation and healing (five verdicts: refuse / refuse-writes /
tolerate / coerce / heal; heal axes consent x timing incl. the inline mode;
write discipline decided by the existing stamp rule; disk re-verify, memo
discipline, defer-under-lock). kind re-ruled: any object may carry it, the
value names the kind, trusted where consulted, no stripping; backfill is the
service's on-touch heal, on-touch only. 02 gains IntegrityRules +
HealScheduler components — rules and policy consolidate, enforcement stays
with loader/writer.

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

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

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

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

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

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

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

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

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

3 tests. 1661 green on both schemes.

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

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

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

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

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

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

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

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

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

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

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

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

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

11 tests added. 1649 green on both schemes.

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

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

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

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

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-29 12:14:47 -04:00