Realign code with the 2026-07-31 findings-resolution rulings
The full bullet list from Implementation card bf080d9a — both ruling batches, including the three appended mid-session by16ef377: - Restore subjects compose the inverse, never nest: crossing "Undo: S" emits "Redo: S" and vice versa; parity, not stack depth, reads a legacy double prefix (GitHistoryProvider.restoreSubject). - Git-operation failures join the one-shot failure banner tier: BannerCenter.GitFailureBanner (undo/redo/branchSwitch/addGit), error tone at failure rank merged with write one-shots by recency; the postLoss compromise is retired at both AppModel wirings. - order/schema optional below the board root: append-at-end reading (ordered siblings first, folder-name tie-break among the order-less), schema reads 1, both coerce-tier logged; the root keeps its requirements. Ranks.resolvedOrders materializes finite ranks so models and placement math stay untouched; first Writer rewrite stamps a real rank on touch, placement against an order-less sibling stamps that sibling inline in the same bracket. Agent guide v10 teaches optional keys and zero-read filing. Hostile-YAML order shapes become coercion tests; Fixtures/Valid/optional-keys.kanban replaces the four retired Malformed boards. - .gitignore is the relocation-heal noise gate: GitignoreRules pure matcher (standard semantics, board-root file only), loader consults it once per walk so matched loose files keep the stray posture; seeded (.DS_Store + .*.lanework-*) at board creation and template instantiation, healed in when missing at open — repo-nested included; empty file honored, existing files never edited; the committer's obedience via libgit2 status is pinned by test. - Comments crash-residue sweep gates on step ownership: HistoryStep derives backing from its own undo expectations, backedContent unions both stacks, the sweep purges per-entry only what no live step owns. - Skip-purge decoupled (16ef377): a stale-skipped coarse step strands whole in NativeHistoryProvider.strandedSteps — still backing, retired only at session end; clean exits purge as before. - Coarse close step named "Changes to '<card>'"; the fine body-edit wording never leaks onto the board menu. - Branch-switch settle clears every open card window's fine stack on Save All and Discard alike; the empty fold registers no coarse step. - Close flush awaits its covering snapshot (quiesce + one generation bump, 1s bound), and an explicit flush now queues behind an in-flight one instead of skipping — the audit-caught interleaving could lose a close flush permanently when the debounce fired inside the close sequence; regression tests force both races. - Commit comment bullets sort chronologically by created, not UUID. - The production-unwired CardBodyEditSession.editSessionDidChange seam is deleted with its seam-only tests. - Composition-root pins: beginSession composes the committer with the store's own EchoLedger and binds the announcer (the miswire class). - Deliberate 06 conformance pass over every 2026-07-31-tagged sentence: fixed Change-custom-key subjects (the retired named generic was the only producer), the unbuilt Replace attachment vocabulary, heal commits now authored Lanework Integrity, the config reader scopes identity to plain [user] sections, add-git re-runs detection at create (a stale mode-none could initialize inside the user's repo), and add-git failures answer at the form or the banner. Structural residue filed on the Redesign board. 2554 tests / 439 suites green. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
@@ -83,6 +83,11 @@ enum CommitMessageEngine {
|
||||
/// container with no identity of its own (`BoardModel.trash`).
|
||||
static let trashDestination = "the trash"
|
||||
|
||||
/// What a key's old (or new) side reads as when the frontmatter simply did not carry it — the
|
||||
/// `(untitled)` convention applied to a value rather than to a name, so "sprint: (none) → 42"
|
||||
/// says *added* without a second sentence shape for it.
|
||||
static let absentValuePlaceholder = "(none)"
|
||||
|
||||
// MARK: - Entry point
|
||||
|
||||
/// One request in, one whole message out — a subject, and a body when there is more to say.
|
||||
@@ -242,8 +247,12 @@ enum CommitMessageEngine {
|
||||
previous: previous.document,
|
||||
current: current.document,
|
||||
noun: "board",
|
||||
// The trio's board phrasing is count-less and title-less ("Relabel board") — there is one
|
||||
// board and naming it adds nothing. The custom-key event is the one place 06 spells the
|
||||
// board's title out ("Change custom key on board '⟨title⟩'"), so it gets it separately.
|
||||
itemTitle: nil,
|
||||
kinds: (.relabelBoard, .assignBoard, .dueBoard, .updateBoard),
|
||||
customKeyItemTitle: title(current.title),
|
||||
kinds: (.relabelBoard, .assignBoard, .dueBoard, .changeKeyBoard),
|
||||
paths: paths
|
||||
)
|
||||
return events
|
||||
@@ -380,7 +389,8 @@ enum CommitMessageEngine {
|
||||
current: new.document,
|
||||
noun: "lane",
|
||||
itemTitle: new.displayTitle,
|
||||
kinds: (.relabelLane, .assignLane, .dueLane, .updateLane),
|
||||
customKeyItemTitle: new.displayTitle,
|
||||
kinds: (.relabelLane, .assignLane, .dueLane, .changeKeyLane),
|
||||
paths: paths
|
||||
)
|
||||
}
|
||||
@@ -637,7 +647,8 @@ enum CommitMessageEngine {
|
||||
current: new.card.document,
|
||||
noun: "card",
|
||||
itemTitle: new.displayTitle,
|
||||
kinds: (.relabelCard, .assignCard, .dueCard, .updateCard),
|
||||
customKeyItemTitle: new.displayTitle,
|
||||
kinds: (.relabelCard, .assignCard, .dueCard, .changeKeyCard),
|
||||
paths: paths
|
||||
)
|
||||
return events
|
||||
@@ -684,9 +695,19 @@ enum CommitMessageEngine {
|
||||
|
||||
/// **The full schema-1 surface, plus the reserved trio, deliberately** (06 ▸ The external gap,
|
||||
/// closed): "label, assignee, and due changes compose … even though 01-storage-format.md reserves
|
||||
/// those keys out of this version's UI — external writers … are exactly who touches them. A change
|
||||
/// to any other unmodeled or custom key composes a named generic ('Update card 'X'') — **never a
|
||||
/// board-level shrug when the touched item is identifiable**."
|
||||
/// those keys out of this version's UI — external writers … are exactly who touches them."
|
||||
///
|
||||
/// **A change to any other unmodeled or custom key says what it is** (06, re-ruled 2026-07-31 —
|
||||
/// "first lines self-describe; generics are a last resort, kept very rare"):
|
||||
///
|
||||
/// > **"Change custom key on card 'X'"** (board and lane likewise — "Change custom key on board
|
||||
/// > '⟨title⟩'"; several keys fold plural), the body naming each key with its old → new values.
|
||||
///
|
||||
/// The named generic it retired ("Update card 'X'") is 06's own last resort and survives only
|
||||
/// "for a change in a known file that is neither a vocabulary event nor a key change — a shape
|
||||
/// that should almost never occur". Nothing composes that shape today: a known file whose diff is
|
||||
/// neither is the *bookkeeping* rule's silence, which composes nothing at all. So the vocabulary
|
||||
/// keeps no case for it, and the zero-event floor stays `unnamedSubject`'s.
|
||||
///
|
||||
/// Read off `unknownFields`, which is exactly "every key the schema does not own" — so the
|
||||
/// bookkeeping keys are excluded by construction rather than by a list kept in step: `modified`,
|
||||
@@ -701,7 +722,8 @@ enum CommitMessageEngine {
|
||||
current: FrontmatterDocument,
|
||||
noun: String,
|
||||
itemTitle: String?,
|
||||
kinds: (label: Kind, assignee: Kind, due: Kind, generic: Kind),
|
||||
customKeyItemTitle: String,
|
||||
kinds: (label: Kind, assignee: Kind, due: Kind, customKey: Kind),
|
||||
paths: [String]
|
||||
) -> [Event] {
|
||||
let before = Dictionary(previous.unknownFields.map { ($0.key, $0.rawValue) }, uniquingKeysWith: { _, last in last })
|
||||
@@ -726,11 +748,24 @@ enum CommitMessageEngine {
|
||||
if changed(Keys.labels) { events.append(event(kinds.label, "Relabel")) }
|
||||
if changed(Keys.assignees) { events.append(event(kinds.assignee, "Assign")) }
|
||||
if changed(Keys.due) { events.append(event(kinds.due, "Set due date on")) }
|
||||
// Everything else the schema does not own — one named generic for the item, however many
|
||||
// custom keys an agent touched in the same window.
|
||||
|
||||
// **Everything else the schema does not own says what it is** (re-ruled 2026-07-31). The
|
||||
// *item* is still what the subject names — one event however many keys moved — but the verb
|
||||
// now describes the change instead of shrugging at it, and the keys themselves ride the body.
|
||||
let trio: Set<String> = [Keys.labels, Keys.assignees, Keys.due]
|
||||
if Set(before.keys).union(after.keys).subtracting(trio).contains(where: changed) {
|
||||
events.append(event(kinds.generic, "Update"))
|
||||
let keys = Set(before.keys).union(after.keys).subtracting(trio).filter(changed).sorted()
|
||||
if !keys.isEmpty {
|
||||
let subject = keys.count == 1 ? "custom key" : "\(keys.count) custom keys"
|
||||
events.append(Event(
|
||||
kind: kinds.customKey,
|
||||
subject: "Change \(subject) on \(noun) \(quotedSubject(customKeyItemTitle))",
|
||||
bullet: "Change \(subject) on \(noun) \(quoted(customKeyItemTitle))",
|
||||
detail: keys
|
||||
.map { "\($0): \(before[$0] ?? absentValuePlaceholder) → \(after[$0] ?? absentValuePlaceholder)" }
|
||||
.joined(separator: "\n"),
|
||||
destination: customKeyItemTitle,
|
||||
paths: paths
|
||||
))
|
||||
}
|
||||
return events
|
||||
}
|
||||
@@ -750,10 +785,17 @@ enum CommitMessageEngine {
|
||||
/// lane is one event, not one line per file inside it);
|
||||
/// - the departing end of a **rename**, which its arrival already speaks for — the loose-file
|
||||
/// relocation, the remint, a displaced squatter.
|
||||
///
|
||||
/// **One path shape is not a generic** (06 ▸ Commit messages ▸ Vocabulary, "Replace added
|
||||
/// 2026-07-31"): "a changed file under a card's `attachments/` with an unchanged listing is a
|
||||
/// content replacement, named from the path alone: 'Replace attachment 'photo.png' — card 'X'',
|
||||
/// never the anonymous path generic". An unchanged listing is exactly "no model event claimed
|
||||
/// this path", so the rule needs no second question of the snapshot — see `replacedAttachment`.
|
||||
private static func pathEvents(for request: CommitMessageRequest, claimedBy model: [Event]) -> [Event] {
|
||||
let claimed = Set(model.flatMap(\.paths))
|
||||
var comments: [String: CommentGroup] = [:]
|
||||
var events: [Event] = []
|
||||
lazy var cardTitles = cardTitlesByPath(request)
|
||||
|
||||
for changed in request.changedPaths.sorted(by: { $0.path < $1.path }) {
|
||||
let path = changed.path
|
||||
@@ -763,7 +805,11 @@ enum CommitMessageEngine {
|
||||
// schema): a trashed card's thread lives under `.trash/`, which the model-silence rule
|
||||
// would otherwise swallow whole.
|
||||
if let comment = CommentPath.classify(path) {
|
||||
comments[CommentGroup.key(comment), default: CommentGroup(comment: comment)].add(changed)
|
||||
let folder = commentFolder(of: path) ?? comment.cardPath
|
||||
comments[
|
||||
CommentGroup.key(comment),
|
||||
default: CommentGroup(comment: comment, folder: folder)
|
||||
].add(changed)
|
||||
continue
|
||||
}
|
||||
guard !Paths.isModelSilent(path) else { continue }
|
||||
@@ -778,6 +824,21 @@ enum CommitMessageEngine {
|
||||
events.append(Event(kind: .agentGuide, subject: "Update agent guide (v\(version))", paths: [path]))
|
||||
continue
|
||||
}
|
||||
|
||||
// **Replace**, before the anonymous generic — the file is still listed, so the model had
|
||||
// nothing to say and the path says it instead.
|
||||
if let replaced = replacedAttachment(at: path) {
|
||||
let card = cardTitles[replaced.cardFolder] ?? untitledPlaceholder
|
||||
events.append(Event(
|
||||
kind: .replaceFile,
|
||||
subject: "Replace attachment \(quotedSubject(replaced.file)) — card \(quotedSubject(card))",
|
||||
bullet: "Replace attachment \(quoted(replaced.file)) — card \(quoted(card))",
|
||||
destination: card,
|
||||
paths: [path]
|
||||
))
|
||||
continue
|
||||
}
|
||||
|
||||
events.append(Event(
|
||||
kind: .updatePath,
|
||||
subject: "Update \(quotedSubject(path))",
|
||||
@@ -788,12 +849,33 @@ enum CommitMessageEngine {
|
||||
return commentEvents(comments, model: model, request: request) + events
|
||||
}
|
||||
|
||||
/// **A card attachment rewritten in place** — the `Replace` shape, read from the path alone.
|
||||
///
|
||||
/// `<lane>/<card>/attachments/<file>`, and nothing else: an attachment one folder deeper is not a
|
||||
/// shape this app writes, and a trashed card's is unreachable here (everything under `.trash/` is
|
||||
/// model-silent and never gets this far). Whether the *listing* changed is the caller's question,
|
||||
/// already answered — an added or removed file was claimed by its own Attach/Remove event before
|
||||
/// this path was ever reached.
|
||||
private static func replacedAttachment(at path: String) -> (cardFolder: String, file: String)? {
|
||||
let components = path.split(separator: "/", omittingEmptySubsequences: true).map(String.init)
|
||||
guard components.count == 4,
|
||||
components[2] == Paths.attachmentsFolder,
|
||||
let folder = cardFolder(of: path)
|
||||
else { return nil }
|
||||
return (folder, components[3])
|
||||
}
|
||||
|
||||
// MARK: - The comment verb family
|
||||
|
||||
/// Every changed path inside **one comment folder**, gathered so a comment that had its
|
||||
/// `index.md` and two attachments rewritten is one event rather than three.
|
||||
private struct CommentGroup {
|
||||
let comment: CommentPath
|
||||
|
||||
/// This comment's own folder, board-root-relative — the key its `created` is looked up under
|
||||
/// (`CommitMessageRequest.commentTimestamps`) and the name the chronology's tie-break reads.
|
||||
let folder: String
|
||||
|
||||
var paths: [String] = []
|
||||
var hasArrival = false
|
||||
var hasSurvivor = false
|
||||
@@ -806,6 +888,11 @@ enum CommitMessageEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// The folder's last component — the UUID a comment is named by, or `.draft`.
|
||||
var folderName: String {
|
||||
folder.split(separator: "/", omittingEmptySubsequences: true).last.map(String.init) ?? folder
|
||||
}
|
||||
|
||||
mutating func add(_ changed: GitChangedPath) {
|
||||
paths.append(changed.path)
|
||||
if changed.isArrival { hasArrival = true }
|
||||
@@ -813,6 +900,29 @@ enum CommitMessageEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// **The comment folder a changed path sits in**, board-root-relative — `<card>/comments/<uuid>`,
|
||||
/// `<card>/comments/.trash/<uuid>` or `<card>/comments/.draft` — or `nil` for a path that is not
|
||||
/// inside a thread at all.
|
||||
///
|
||||
/// Shared with the flush that reads each of those folders' `created`
|
||||
/// (`GitAutoCommitter.commentTimestamps(for:boardRoot:)`), so the key a timestamp is *filed*
|
||||
/// under and the key it is *looked up* by have one definition. Everything about where a thread
|
||||
/// lives is still `CommentPath.classify`'s; this only says how many of the path's components that
|
||||
/// classification consumed.
|
||||
static func commentFolder(of path: String) -> String? {
|
||||
guard let comment = CommentPath.classify(path) else { return nil }
|
||||
let components = path.split(separator: "/", omittingEmptySubsequences: true).map(String.init)
|
||||
// `classify` has already guaranteed these counts — `<lane>/<card>/comments/<entry>` for a
|
||||
// comment or the draft, one deeper for `comments/.trash/<uuid>`.
|
||||
let depth: Int
|
||||
switch comment.kind {
|
||||
case .comment, .draft: depth = 4
|
||||
case .trashed: depth = 5
|
||||
}
|
||||
guard components.count >= depth else { return nil }
|
||||
return components.prefix(depth).joined(separator: "/")
|
||||
}
|
||||
|
||||
/// **The comment verb family** (01-storage-format.md § Enhanced schema, the `kind: comment` block:
|
||||
/// "foreign comment changes are described by **path shape** — the 'Update agent guide (vN)'
|
||||
/// mechanism: a changed path under `…/comments/<uuid>/` composes 'Comment on ⟨card title⟩' / 'Edit
|
||||
@@ -850,7 +960,7 @@ enum CommitMessageEngine {
|
||||
})
|
||||
|
||||
var events: [Event] = []
|
||||
for key in groups.keys.sorted() {
|
||||
for key in chronological(groups, timestamps: request.commentTimestamps) {
|
||||
guard let group = groups[key], !group.paths.isEmpty else { continue }
|
||||
guard !relocated.contains(group.comment.cardPath) else { continue }
|
||||
let card = titles[group.comment.cardPath] ?? untitledPlaceholder
|
||||
@@ -891,6 +1001,47 @@ enum CommitMessageEngine {
|
||||
return events
|
||||
}
|
||||
|
||||
/// **A commit's comment bullets sort chronologically** (06-history-undo.md ▸ Rules ▸ Auto-commit,
|
||||
/// blessed 2026-07-31):
|
||||
///
|
||||
/// > "by the comments' own `created`, folder name on ties — event order reads as the conversation
|
||||
/// > did, never UUID-arbitrary."
|
||||
///
|
||||
/// The ladder is `CommentThread.sorted`'s, one layer up and over folders rather than over parsed
|
||||
/// comments: `created` ascending, **the undated after the dated**, folder name on ties — compared
|
||||
/// as the canonical lowercase spelling, the corpus-wide rule for every folder-name tie-break
|
||||
/// (01-storage-format.md § Ordering). The thread the bullets describe is sorted by that ladder on
|
||||
/// screen; a commit describing the same thread in a different order would be the same events told
|
||||
/// out of sequence.
|
||||
///
|
||||
/// **Chronology is global rather than per card**, recorded as a judgment call. A window's comment
|
||||
/// events almost always belong to one card — the card-window session flush is that window by
|
||||
/// construction — and where they do not, "the order the events happened in" is still the honest
|
||||
/// reading of a list of events; grouping by card first would sort a conversation by which lane its
|
||||
/// card sits in. The group key breaks the last tie so the order is total, which keeps a message
|
||||
/// reproducible rather than hash-ordered.
|
||||
private static func chronological(
|
||||
_ groups: [String: CommentGroup],
|
||||
timestamps: [String: Date]
|
||||
) -> [String] {
|
||||
groups.keys.sorted { lhs, rhs in
|
||||
guard let left = groups[lhs], let right = groups[rhs] else { return lhs < rhs }
|
||||
switch (timestamps[left.folder], timestamps[right.folder]) {
|
||||
case let (leftDate?, rightDate?) where leftDate != rightDate:
|
||||
return leftDate < rightDate
|
||||
case (.some, .none):
|
||||
return true
|
||||
case (.none, .some):
|
||||
return false
|
||||
default:
|
||||
break
|
||||
}
|
||||
let leftName = IntegrityRules.canonicalIdentity(left.folderName)
|
||||
let rightName = IntegrityRules.canonicalIdentity(right.folderName)
|
||||
return leftName == rightName ? lhs < rhs : leftName < rightName
|
||||
}
|
||||
}
|
||||
|
||||
/// The card folders this commit says moved, arrived or went — the set a comment path checks
|
||||
/// itself against before speaking.
|
||||
private static func relocatedCardFolders(in model: [Event]) -> Set<String> {
|
||||
@@ -962,17 +1113,17 @@ enum CommitMessageEngine {
|
||||
|
||||
/// The verb-plus-noun grouping that decides what folds with what — **06's vocabulary, one case
|
||||
/// each**: Add / Delete / Move / Rename / Edit / Restyle / Resize / Reorder over cards, lanes and
|
||||
/// the board, Attach / Remove for attachment files, Repair for the remint, the trash pair, the
|
||||
/// reserved metadata trio, and the two path shapes.
|
||||
/// the board, Attach / Remove / **Replace** for attachment files, Repair for the remint, the trash
|
||||
/// pair, the reserved metadata trio, the custom-key change, and the two path shapes.
|
||||
enum Kind: Hashable {
|
||||
case addCard, deleteCard, restoreCard, purgeCard, moveCard, renameCard, editCard, restyleCard
|
||||
case relabelCard, assignCard, dueCard, updateCard
|
||||
case attachFile, removeFile, reorderCards, repairDuplicate
|
||||
case relabelCard, assignCard, dueCard, changeKeyCard
|
||||
case attachFile, removeFile, replaceFile, reorderCards, repairDuplicate
|
||||
|
||||
case addLane, deleteLane, restoreLane, purgeLane, renameLane, editLane, restyleLane, resizeLane
|
||||
case relabelLane, assignLane, dueLane, updateLane, reorderLanes
|
||||
case relabelLane, assignLane, dueLane, changeKeyLane, reorderLanes
|
||||
|
||||
case renameBoard, editBoard, restyleBoard, relabelBoard, assignBoard, dueBoard, updateBoard
|
||||
case renameBoard, editBoard, restyleBoard, relabelBoard, assignBoard, dueBoard, changeKeyBoard
|
||||
|
||||
case agentGuide, updatePath
|
||||
|
||||
@@ -1014,13 +1165,18 @@ enum CommitMessageEngine {
|
||||
case .relabelCard: return "Relabel \(count) cards"
|
||||
case .assignCard: return "Assign \(count) cards"
|
||||
case .dueCard: return "Set due date on \(count) cards"
|
||||
case .updateCard: return "Update \(count) cards"
|
||||
case .changeKeyCard:
|
||||
guard let destination else { return "Change custom keys on \(count) cards" }
|
||||
return "Change custom keys on card \(CommitMessageEngine.quotedSubject(destination))"
|
||||
case .attachFile:
|
||||
guard let destination else { return "Attach \(count) files" }
|
||||
return "Attach \(count) files to card \(CommitMessageEngine.quotedSubject(destination))"
|
||||
case .removeFile:
|
||||
guard let destination else { return "Remove \(count) files" }
|
||||
return "Remove \(count) files from card \(CommitMessageEngine.quotedSubject(destination))"
|
||||
case .replaceFile:
|
||||
guard let destination else { return "Replace \(count) attachments" }
|
||||
return "Replace \(count) attachments — card \(CommitMessageEngine.quotedSubject(destination))"
|
||||
case .reorderCards:
|
||||
guard let destination else { return "Reorder cards in \(count) lanes" }
|
||||
return "Reorder cards in \(CommitMessageEngine.truncated(destination))"
|
||||
@@ -1037,7 +1193,9 @@ enum CommitMessageEngine {
|
||||
case .relabelLane: return "Relabel \(count) lanes"
|
||||
case .assignLane: return "Assign \(count) lanes"
|
||||
case .dueLane: return "Set due date on \(count) lanes"
|
||||
case .updateLane: return "Update \(count) lanes"
|
||||
case .changeKeyLane:
|
||||
guard let destination else { return "Change custom keys on \(count) lanes" }
|
||||
return "Change custom keys on lane \(CommitMessageEngine.quotedSubject(destination))"
|
||||
|
||||
// A board has one title, one description, one style — and a lane reorder is a single
|
||||
// whole-board event. None of these can actually recur; the switch stays exhaustive.
|
||||
@@ -1048,7 +1206,11 @@ enum CommitMessageEngine {
|
||||
case .relabelBoard: return "Relabel board"
|
||||
case .assignBoard: return "Assign board"
|
||||
case .dueBoard: return "Set due date on board"
|
||||
case .updateBoard: return CommitMessageEngine.unnamedSubject
|
||||
// One board, so a plural of *events* here is a plural of keys already folded into one
|
||||
// event — unreachable in practice, and count-less if it ever is.
|
||||
case .changeKeyBoard:
|
||||
guard let destination else { return "Change custom keys on board" }
|
||||
return "Change custom keys on board \(CommitMessageEngine.quotedSubject(destination))"
|
||||
|
||||
case .agentGuide: return "Update agent guide"
|
||||
case .updatePath: return "Update \(count) files"
|
||||
@@ -1108,7 +1270,9 @@ enum CommitMessageEngine {
|
||||
/// Every `index.md` in the board's fractal layout, and everything inside `.trash/`. An
|
||||
/// attachment is deliberately **not** here: the model carries attachment *names*, so an added
|
||||
/// or removed file composes its own event, while a rewritten one — same name, new bytes — has
|
||||
/// nothing in the snapshot to show for it and rightly composes "Update '⟨path⟩'".
|
||||
/// nothing in the snapshot to show for it and composes **Replace** off its path instead
|
||||
/// (`replacedAttachment(at:)`, 06 ▸ Commit messages ▸ Vocabulary, added 2026-07-31 — "never
|
||||
/// the anonymous path generic").
|
||||
/// Whether a path could make the *snapshot* differ at all — every path the model speaks for,
|
||||
/// plus attachments, whose names it carries.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user