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
+22 -5
View File
@@ -90,10 +90,21 @@ public struct GitChangedPath: Sendable, Equatable, Hashable {
/// deletion on disk that the window must not be demoted by.
public let isRename: Bool
public init(path: String, isDeletion: Bool, isRename: Bool) {
/// Whether the path is **new in this commit** git's own `GIT_DELTA_ADDED` (and a rename's
/// arriving end), surfaced rather than inferred.
///
/// It exists for the comment verb family (01-storage-format.md § Enhanced schema): comments are
/// window-scoped and the board snapshot never carries them, so "Comment on 'X'" and "Edit comment
/// on 'X'" cannot be told apart by a diff of two snapshots the only thing that distinguishes a
/// comment folder arriving from one being rewritten is whether HEAD already had it, which is
/// exactly the question this diff already answered.
public let isArrival: Bool
public init(path: String, isDeletion: Bool, isRename: Bool, isArrival: Bool = false) {
self.path = path
self.isDeletion = isDeletion
self.isRename = isRename
self.isArrival = isArrival
}
}
@@ -331,7 +342,7 @@ enum GitCommitOperation {
var found: [String: GitChangedPath] = [:]
func record(_ path: String?, isDeletion: Bool, isRename: Bool) {
func record(_ path: String?, isDeletion: Bool, isRename: Bool, isArrival: Bool = false) {
guard let path, !path.isEmpty else { return }
let existing = found[path]
found[path] = GitChangedPath(
@@ -339,7 +350,10 @@ enum GitCommitOperation {
// Present wins where two deltas disagree: staging asks "is it there now", and the
// `modified-by` demotion must not fire for a file the window ends with.
isDeletion: (existing?.isDeletion ?? true) && isDeletion,
isRename: (existing?.isRename ?? false) || isRename
isRename: (existing?.isRename ?? false) || isRename,
// New wins, for the mirror of that reason: one delta calling a path an addition is
// enough to know HEAD did not have it, which is the whole content of the bit.
isArrival: (existing?.isArrival ?? false) || isArrival
)
}
@@ -350,9 +364,12 @@ enum GitCommitOperation {
record(string(delta.old_file.path), isDeletion: true, isRename: false)
case GIT_DELTA_RENAMED:
// Both ends, and neither is a deletion the window may be demoted by: the departure
// has to leave the index and the arrival has to enter it.
// has to leave the index and the arrival has to enter it. The arriving end is new at
// its path, which is what the comment family reads a post by.
record(string(delta.old_file.path), isDeletion: true, isRename: true)
record(string(delta.new_file.path), isDeletion: false, isRename: true)
record(string(delta.new_file.path), isDeletion: false, isRename: true, isArrival: true)
case GIT_DELTA_ADDED, GIT_DELTA_COPIED, GIT_DELTA_UNTRACKED:
record(string(delta.new_file.path), isDeletion: false, isRename: false, isArrival: true)
default:
record(string(delta.new_file.path), isDeletion: false, isRename: false)
}