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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user