The message engine outlives its substrate — harvested to Kanban/Changes/ as the change narrator

Step 2 of strategy/01-git-excision.md: CommitMessageEngine and the composer seam relocate to a neutral module renamed away from commit vocabulary (ChangeNarrator, ChangeNarrationRequest, ChangeNarrating, SemanticChangeNarration, ChangeAuthorship), GitChangedPath extracts from GitCommitOperation as ChangedPath, and the one git tie severs — authorship's foreign case carries a display name, not a GitIdentity. The spec tests transplant as ChangeNarratorTests, alive until the journal work begins. 3,009 tests green.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-08 10:38:43 -04:00
parent 6d1872ad8b
commit ae7be98eaa
8 changed files with 207 additions and 182 deletions
+10 -10
View File
@@ -619,14 +619,14 @@ struct AutoCommitStageAroundTests {
/// A composer that takes its time, so a test can hold a flush open and drive the close sequence into
/// the gap. Everything else about it is the real one this suite asserts *when* a commit exists, and
/// a fake message would make the commits it reads back unrecognisable.
private struct SlowComposer: CommitMessageComposing {
private struct SlowComposer: ChangeNarrating {
let delay: TimeInterval
func message(for request: CommitMessageRequest) -> String {
func narrative(for request: ChangeNarrationRequest) -> String {
// Blocking, deliberately: this runs on the flush's own detached task, and what the test needs
// held open is that task rather than the actor the close sequence is running on.
Thread.sleep(forTimeInterval: delay)
return CommitMessageEngine.message(for: request)
return ChangeNarrator.narrative(for: request)
}
}
@@ -1336,7 +1336,7 @@ struct AutoCommitMessageTests {
/// its subject describes a board that has not heard about the card it is committing.
@Test("Without the await the subject is the one the stale snapshot could compose — the defect, pinned")
func aStaleSnapshotComposesTheShrug() async throws {
#expect(try await flushRacingItsReload(awaitsCoverage: false) == CommitMessageEngine.unnamedSubject)
#expect(try await flushRacingItsReload(awaitsCoverage: false) == ChangeNarrator.unnamedSubject)
}
/// The bound is a bound: a board whose watcher stream never came up has no reload to wait for, and
@@ -1345,7 +1345,7 @@ struct AutoCommitMessageTests {
func theWaitIsBounded() async throws {
// The generation never moves, so the wait runs to its (millisecond) deadline and composes.
#expect(try await flushRacingItsReload(awaitsCoverage: true, landsAfterReads: .max)
== CommitMessageEngine.unnamedSubject)
== ChangeNarrator.unnamedSubject)
}
}
@@ -1380,8 +1380,8 @@ struct CommitAttributionRuleTests {
try fixture.item("lane/card", stamped("Moved", by: "claude"))
let paths = [
GitChangedPath(path: "old/card/index.md", isDeletion: true, isRename: true),
GitChangedPath(path: "lane/card/index.md", isDeletion: false, isRename: true)
ChangedPath(path: "old/card/index.md", isDeletion: true, isRename: true),
ChangedPath(path: "lane/card/index.md", isDeletion: false, isRename: true)
]
// "**A folder move is not a deletion**: items match by id across the whole board."
#expect(CommitAttribution.foreignIdentity(for: paths, under: fixture.root).name == "claude")
@@ -1391,7 +1391,7 @@ struct CommitAttributionRuleTests {
func departuresAloneFallBack() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let paths = [GitChangedPath(path: "old/card/index.md", isDeletion: true, isRename: true)]
let paths = [ChangedPath(path: "old/card/index.md", isDeletion: true, isRename: true)]
#expect(CommitAttribution.foreignIdentity(for: paths, under: fixture.root)
== CommitAttribution.externalIdentity)
}
@@ -1417,7 +1417,7 @@ struct CommitAttributionRuleTests {
let file = EchoLedger.key(fixture.url("lane/card").appendingPathComponent("index.md"))
let matching = [file: HarvestedReceipt(receipt: .content(hash: EchoLedger.hash(of: text)), isHeal: false)]
let stale = [file: HarvestedReceipt(receipt: .content(hash: EchoLedger.hash(of: "other")), isHeal: false)]
let changed = [GitChangedPath(path: "lane/card/index.md", isDeletion: false, isRename: false)]
let changed = [ChangedPath(path: "lane/card/index.md", isDeletion: false, isRename: false)]
#expect(CommitAttribution.split(changed, under: fixture.root, receipts: matching).user == changed)
// "a foreign edit landing on an app-written path inside the same window misses the hash and
@@ -1436,7 +1436,7 @@ struct CommitAttributionRuleTests {
let file = EchoLedger.key(fixture.url("lane/card").appendingPathComponent("index.md"))
let receipts = [file: HarvestedReceipt(receipt: .content(hash: EchoLedger.hash(of: text)), isHeal: true)]
let changed = [GitChangedPath(path: "lane/card/index.md", isDeletion: false, isRename: false)]
let changed = [ChangedPath(path: "lane/card/index.md", isDeletion: false, isRename: false)]
let split = CommitAttribution.split(changed, under: fixture.root, receipts: receipts)
#expect(split.heal == changed)