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
This commit is contained in:
2026-07-31 14:54:17 -04:00
parent 3c07c26fda
commit 563999655f
8 changed files with 2479 additions and 50 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ Lanework is in early development. This list tracks what has actually shipped and
- **Git integration (Lanework Pro)** — git is **opt-in per board and never silent**. Under a subscription, a board's mode is detected freshly at every open, nearest-`.git`-wins: a `.git` at the board root means git mode, a `.git` only further up means the board lives inside somebody else's repository, and neither means no git at all. Detection is an open-time fact by design — a `git init` run in a terminal under an open board takes effect the next time you open it, and nothing watches for a repository appearing. **Adoption is not initialization**: a board whose folder already holds a repository (you cloned it, or you ran `git init` yourself) simply opens in git mode, with no dialog and no adoption step — the repository's presence *is* the opt-in, which is how a second machine joins a shared board. **Add Git** in the board popover is the only thing in the app that ever creates one: it initializes a repository in the board's folder and immediately commits the whole tree as "Initial board state", so the board is protected from the moment git exists, and it flips the open board into git mode on the spot. A board inside an existing repository is left strictly alone — no nested repository, no commits into your project — and the popover says so in plain words instead of showing a disabled button. The git client is **bundled** (libgit2, in-process via SwiftGitX): nothing here shells out, and none of it needs git installed. Git history also settles duplicate-id collisions on git boards — of two folders claiming one identity, the path that entered history first wins.
- **Auto-commit (Lanework Pro)** — on a git board **every settled change becomes a commit**, debounced a couple of seconds past the churn of a drag or a burst of typing, so one gesture is one commit rather than forty. Agent and hand edits ride the same debounce, so work done in a terminal or by an agent is committed, attributed and undoable exactly like your own. What commits is the **tree**, not the app's model: strays, `CLAUDE.md` and anything else `git status` shows go in too (your `.gitignore` is respected), so a board is never left quietly dirty. **Commit authorship is structural**: the app knows, file by file, what it wrote and what it didn't, so your changes are authored as you while changes from outside are authored as `Lanework External <[email protected]>` — and when every file in an outside batch carries the same `modified-by:` stamp, that batch is authored as that agent instead. A window holding both kinds is split into two commits rather than mixed, outside changes first, and a scheduled repair's files commit separately again. Identity comes from the repository's own `.git/config` when it names one, and otherwise from your macOS account name and machine. **The card editor is the exception, deliberately**: its 700 ms saves keep the file crash-safe but stay uncommitted for the length of a session, and the body lands as exactly one commit when you leave Edit — so a lane move made while you're typing commits the move and steps around the card you're in. Closing a board window or quitting flushes everything pending before teardown, and a board opened with uncommitted changes commits them through the same engine. When another program holds the repository's index, the committer waits, retries briefly, and then simply tries again at the next quiet moment — never an error, and the lock is never removed, because it isn't the app's. A merge, rebase, cherry-pick or detached HEAD left by outside-the-app git **pauses** committing entirely rather than writing into a state the app didn't create; your edits keep landing on disk and commit as one batch when you've finished up in whichever tool started it. Messages read "Update board" for now — the semantic message engine ("Move card 'Fix login' to Doing") is the next card. Still ahead in pro-m1/m2: git-backed undo/redo, branch switching, `.gitignore` seeding, and remotes.
- **Auto-commit (Lanework Pro)** — on a git board **every settled change becomes a commit**, debounced a couple of seconds past the churn of a drag or a burst of typing, so one gesture is one commit rather than forty. Agent and hand edits ride the same debounce, so work done in a terminal or by an agent is committed, attributed and undoable exactly like your own. What commits is the **tree**, not the app's model: strays, `CLAUDE.md` and anything else `git status` shows go in too (your `.gitignore` is respected), so a board is never left quietly dirty. **Commit authorship is structural**: the app knows, file by file, what it wrote and what it didn't, so your changes are authored as you while changes from outside are authored as `Lanework External <[email protected]>` — and when every file in an outside batch carries the same `modified-by:` stamp, that batch is authored as that agent instead. A window holding both kinds is split into two commits rather than mixed, outside changes first, and a scheduled repair's files commit separately again. Identity comes from the repository's own `.git/config` when it names one, and otherwise from your macOS account name and machine. **The card editor is the exception, deliberately**: its 700 ms saves keep the file crash-safe but stay uncommitted for the length of a session, and the body lands as exactly one commit when you leave Edit — so a lane move made while you're typing commits the move and steps around the card you're in. Closing a board window or quitting flushes everything pending before teardown, and a board opened with uncommitted changes commits them through the same engine. When another program holds the repository's index, the committer waits, retries briefly, and then simply tries again at the next quiet moment — never an error, and the lock is never removed, because it isn't the app's. A merge, rebase, cherry-pick or detached HEAD left by outside-the-app git **pauses** committing entirely rather than writing into a state the app didn't create; your edits keep landing on disk and commit as one batch when you've finished up in whichever tool started it. **What each commit says is composed, not templated**: at commit time the app diffs the board as HEAD has it against the board as it is now — never by watching what you clicked — and writes what actually happened. "Move card 'Fix login' to Doing". "Rename lane 'Todo' → 'Doing'". "Relabel card 'Fix login'". Items are matched by identity across the whole board, so a card dragged between lanes reads as a move rather than a deletion beside an addition, and a folder moved by hand in the Finder reads exactly the same. Several changes of one kind fold into one line with the destination kept ("Move 3 cards to Done"); a genuinely mixed batch reads "Update board" with every event listed underneath, so `git log --oneline` stays scannable and the full message stays complete. Deleting a lane says "Delete lane 'Todo'" with its cards as detail rather than burying the event in a count. The trash is told apart by shape alone: into `.trash/` is "Delete card 'X'", back out is "Restore card 'X'", and gone for good is "Permanently delete card 'X'" — so the log distinguishes moved-to-trash from gone-forever without being told which gesture you used. Files the board model doesn't cover are described too rather than silently swept in: a changed agent guide reads "Update agent guide (v7)", a comment gets its own verbs off its path shape — "Comment on 'Fix login'", "Edit comment on…", "Delete comment on…", and the quiet "Draft comment on…" for the composer's slow saves — and any other stray reads "Update 'notes.txt'". Bookkeeping stays out of it — a bumped timestamp, a rank rescale, or a backfilled key composes nothing. And because the origin of a change lives in the author field, **an agent's commit reads in exactly the same words as your own**. Still ahead in pro-m1/m2: git-backed undo/redo, branch switching, `.gitignore` seeding, and remotes.
## Development