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 by 16ef377:

- 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:
2026-08-01 07:43:45 -04:00
parent 16ef3779e8
commit 274ccd9ff5
75 changed files with 5619 additions and 791 deletions
+191 -18
View File
@@ -33,13 +33,31 @@ private struct Window {
private let cardID = ItemID(rawValue: Ident.card1)
private let cardPath = "\(Ident.lane1)/\(Ident.card1)"
/// **The coarse close step's row, spelled once** "Changes to 'card'" (13-native-undo.md Rules,
/// ruled 2026-07-31), over the fixture card's own title. Pinned as a value here and asserted verbatim
/// in `theCoarseStepNamesItsCard` below, so a suite that reads the phrase eleven times still only
/// *decides* it once.
private let coarseStep = "Changes to 'Fix login'"
/// A card window over a board with one card, wired exactly as `CardWindowHost` wires one.
@MainActor
private func makeWindow(_ fixture: WriterFixture) throws -> Window {
let store = try BoardStore(rootURL: fixture.root)
let board = NativeHistoryProvider()
store.history = board
return try openWindow(fixture, store: store, board: board)
}
/// **The same card opened again, over the board that is already holding the last session's step**
/// the reopen 13's sweep gate is about (ruled 2026-07-31): a second window is a second session with
/// its own empty stack, and the *board's* stack is the one that survived the close.
@MainActor
private func reopen(_ window: Window, _ fixture: WriterFixture) throws -> Window {
try openWindow(fixture, store: window.store, board: window.board)
}
@MainActor
private func openWindow(_ fixture: WriterFixture, store: BoardStore, board: NativeHistoryProvider) throws -> Window {
let session = CardWindowSession()
CardWindowHost.configureUndo(session, store: store, cardID: cardID)
CardWindowHost.configureComments(session.comments, store: store, cardID: cardID, on: session.undo)
@@ -221,7 +239,7 @@ struct CardSessionCloseTests {
// One step, named for the session rather than for any gesture inside it.
#expect(window.board.canUndo)
#expect(window.board.undoActionName == "Edit Card")
#expect(window.board.undoActionName == coarseStep)
window.board.undo()
#expect(!window.board.canUndo, "exactly one")
@@ -238,6 +256,31 @@ struct CardSessionCloseTests {
#expect(fixture.exists("\(card)/comments/.trash/\(CommentIdent.one)"))
}
@Test("The coarse step is named for its card — 'Changes to ⟨title⟩', never the fine 'Edit Card'")
func theCoarseStepNamesItsCard() async throws {
// 13-native-undo.md Rules, ruled 2026-07-31: "one coarse step named 'Changes to card'
// the board row reads 'Undo Changes to Fix login': plural and scope-flavoured, distinct from
// every fine verb the fine body-edit wording never leaks onto the board menu."
let fixture = try WriterFixture()
defer { fixture.tearDown() }
_ = try makeCommentBoard(fixture)
let window = try makeWindow(fixture)
editBody(window, to: "Edited in the window.\n")
#expect(window.window.stack.undoActionName == "Edit Card",
"the fine body edit keeps its verb — this is the collision the ruling resolves")
await window.session.endSession()
#expect(window.board.undoActionName == "Changes to 'Fix login'")
#expect(window.board.undoActionName == coarseStep)
// The "Undo " prefix is the platform's, composed over the bare phrase (`BoardUndoManager`).
#expect(BoardUndoManager(history: window.board).undoMenuItemTitle == "Undo Changes to 'Fix login'")
// An untitled card renders the placeholder its own window title bar renders "Untitled" is a
// rendering, never a value (03-board-ui.md § Card face).
#expect(HistoryPhrase.cardSession(nil) == "Changes to 'Untitled'")
}
@Test("A session with no net change registers nothing")
func noNetChangeRegistersNothing() async throws {
let fixture = try WriterFixture()
@@ -291,7 +334,7 @@ struct CardSessionCloseTests {
window.comments.commitEdit()
window.comments.delete(ItemID(rawValue: CommentIdent.one))
await window.session.endSession()
#expect(window.board.undoActionName == "Edit Card")
#expect(window.board.undoActionName == coarseStep)
window.board.undo()
#expect(window.store.banners.signposts.isEmpty, "the session's own step is never stale on arrival")
@@ -340,7 +383,7 @@ struct CardSessionCloseTests {
// "A tracked relocation a lane move mid-session or after close, **a trash move** never
// stales the step" (13 Rules, ruled 2026-07-31): the session resolves through the walk that
// spans both containers, so the step registers over the delete rather than being dropped.
#expect(window.board.undoActionName == "Edit Card")
#expect(window.board.undoActionName == coarseStep)
window.board.undo()
#expect(window.store.banners.signposts.isEmpty, "nothing about the card's content changed")
@@ -384,7 +427,7 @@ struct CardSessionCloseTests {
#expect(!window.board.canUndo)
await window.session.endSession()
#expect(window.board.undoActionName == "Edit Card")
#expect(window.board.undoActionName == coarseStep)
window.board.undo()
let document = try FrontmatterDocument.parse(fixture.indexText(cardPath))
@@ -500,7 +543,7 @@ struct CardSessionAnchorTests {
#expect(window.board.undoActionName == "Move Card")
await window.session.endSession()
#expect(window.board.undoActionName == "Edit Card", "the session registered over the move")
#expect(window.board.undoActionName == coarseStep, "the session registered over the move")
window.board.undo()
#expect(window.store.banners.signposts.isEmpty, "a relocation is not a collision")
@@ -544,8 +587,8 @@ struct CardSessionAnchorTests {
#expect(try body(fixture, cardPath) == "Somebody else.\n", "never applied over a newer write")
#expect(!fixture.exists("\(cardPath)/comments/\(CommentIdent.one)"),
"and the comment half did not half-happen either")
#expect(try fixture.entryNames("\(cardPath)/comments/.trash").isEmpty,
"the skipped step retired, so the backing it was holding was purged with it")
#expect(fixture.exists("\(cardPath)/comments/.trash/\(CommentIdent.one)"),
"the skip is not a clean exit — the backing it held survives to the session's end")
#expect(!window.board.canUndo, "both steps are gone — one skipped, one applied")
}
@@ -589,6 +632,34 @@ struct CardSessionPurgeTests {
return (window, card)
}
/// The same close, collided with **the coarse step popped as stale, having applied nothing**,
/// which is the state the skip-purge decoupling is about (13 Interaction with the trash, ruled
/// 2026-07-31).
///
/// **The body edit is what makes this a skip at all.** A session of nothing but the delete names
/// only the comment, and a foreign write to the *card* would leave it perfectly current the undo
/// would then apply, restore the comment, and empty the trash by moving its one entry out, which
/// is the same disk state for entirely the wrong reason. With the body in the step, a foreign body
/// rewrite stales it **without touching what the purge would remove**, which is the only way to
/// ask what the skip did to the backing.
@MainActor
private func skippedOnAForeignEdit(_ fixture: WriterFixture) async throws -> (Window, String) {
let card = try makeCommentBoard(fixture)
try fixture.item(commentPath(CommentIdent.one, inCard: card), commentText())
let window = try makeWindow(fixture)
window.comments.reload()
editBody(window, to: "Edited in the window.\n")
window.comments.delete(ItemID(rawValue: CommentIdent.one))
await window.session.endSession()
_ = try BoardWriter.writeBody(inItemFolder: fixture.url(cardPath), body: "Somebody else.\n")
window.board.undo()
#expect(window.store.banners.signposts.map(\.message)
== ["Undo skipped — 'Fix login' changed outside Lanework"])
#expect(!window.board.canUndo, "the stale step was popped")
return (window, card)
}
@Test("comments/.trash survives the close while the coarse step lives")
func theTrashOutlivesTheClose() async throws {
let fixture = try WriterFixture()
@@ -600,6 +671,64 @@ struct CardSessionPurgeTests {
"the step's undo restores from here — the purge waits for it")
}
// MARK: The reopen 13's sweep gate
@Test("Reopening the card window never destroys the last session's undo backing")
func theReopenSweepSparesTheCoarseStepsBacking() async throws {
// The ship-blocker this gate exists for (13 Interaction with the trash, ruled 2026-07-31):
// the open-time residue sweep used to empty `comments/.trash/` unconditionally, so opening the
// card again threw away the folder the board's own coarse step was about to restore from.
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let (window, card) = try await closedWithADeletedComment(fixture)
let path = commentPath(CommentIdent.one, inCard: card)
// The reopen. `CardComments.open()` runs the sweep before it reads the thread, exactly as the
// host wires it so this is the production sequence, not a re-typed copy of it.
_ = try reopen(window, fixture)
#expect(fixture.exists("\(card)/comments/.trash/\(CommentIdent.one)"),
"the sweep asked the board's stack first: this is backing, not residue")
window.board.undo()
#expect(window.store.banners.signposts.isEmpty, "nothing was stale — the backing was still there")
#expect(fixture.exists(path), "the coarse step's undo restored the comment the reopen spared")
}
@Test("Residue no live step owns still sweeps at the open, beside backing that does")
func unownedResidueStillSweeps() async throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let (window, card) = try await closedWithADeletedComment(fixture)
// A crashed session's leftovers, landing beside the live step's backing: nothing on the stack
// names this one, so it is residue by the ruling's own definition.
try fixture.item("\(card)/comments/.trash/\(CommentIdent.two)", commentText())
_ = try reopen(window, fixture)
#expect(!fixture.exists("\(card)/comments/.trash/\(CommentIdent.two)"), "unowned content sweeps as before")
#expect(fixture.exists("\(card)/comments/.trash/\(CommentIdent.one)"), "and owned content is left alone")
#expect(window.store.heals.memo(for: .commentTrashResidue) == nil,
"the signature is the entries actually purged, and it cleared on success")
}
@Test("Once the hold ends, the purge the sweep deferred to runs")
func theRetirementRunsThePurgeTheSweepDeferredTo() async throws {
// "One condition, two consumers" (13): the sweep spared this content because a step owned it,
// and the moment that ownership ends is the moment the deferred purge was always waiting for.
// The arc spared, then released, then purged is what this proves; *which* release ends it
// is the two tests below and the two below the divider.
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let (window, card) = try await closedWithADeletedComment(fixture)
_ = try reopen(window, fixture)
#expect(fixture.exists("\(card)/comments/.trash/\(CommentIdent.one)"), "spared by the reopen's sweep")
window.board.clear()
#expect(try fixture.entryNames("\(card)/comments/.trash").isEmpty,
"and the purge the sweep had deferred to ran with it")
}
@Test("The board session's end purges what the step was holding")
func theBoardSessionsEndPurges() async throws {
let fixture = try WriterFixture()
@@ -611,19 +740,63 @@ struct CardSessionPurgeTests {
#expect(try fixture.entryNames("\(card)/comments/.trash").isEmpty)
}
@Test("A stale step's skip purges too — the step is gone, so its backing is not needed")
func aSkippedStepPurges() async throws {
// MARK: The skip the one exit that is not clean
@Test("A stale skip purges nothing — the backing it was holding survives")
func aSkippedStepsBackingSurvives() async throws {
// The decoupling ruled 2026-07-31 (13 Interaction with the trash), and its reason: "the skip
// banner says nothing was applied, and an irreversible purge riding that gesture would be
// surprise loss the skip is exactly when the user may want to inspect what the collision
// left". This is the pin that used to say the opposite.
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let (window, card) = try await closedWithADeletedComment(fixture)
let (_, card) = try await skippedOnAForeignEdit(fixture)
// A foreign delete of the trashed folder is not the interesting collision; a foreign body
// rewrite is it makes the step stale without touching what the purge would remove.
_ = try BoardWriter.writeBody(inItemFolder: fixture.url(cardPath), body: "Somebody else.\n")
// The session wrote the body too, so the step names it.
window.board.undo()
#expect(!fixture.exists(commentPath(CommentIdent.one, inCard: card)),
"the step skipped rather than applied — nothing was restored")
#expect(fixture.exists("\(card)/comments/.trash/\(CommentIdent.one)"),
"and nothing was destroyed either: the bytes are still there to look at")
}
#expect(!window.board.canUndo)
@Test("Reopening the card after a skip spares the survivor — the same hold, read twice")
func theSweepSparesASkipSurvivor() async throws {
// The half that makes "survives to board-session end" true rather than merely intended: the
// step is off both stacks, so a sweep that read only those would call this residue and purge
// it at the very next open taking the ruling back one window later.
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let (window, card) = try await skippedOnAForeignEdit(fixture)
_ = try reopen(window, fixture)
#expect(fixture.exists("\(card)/comments/.trash/\(CommentIdent.one)"),
"the sweep asked the same hold the purge is still waiting on")
}
@Test("Unowned residue beside a skip's survivor still sweeps")
func residueSweepsBesideASkipSurvivor() async throws {
// The gate did not become "spare everything in there": a stranded step's claim is exactly as
// narrow as a live one's, one anchor at a time.
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let (window, card) = try await skippedOnAForeignEdit(fixture)
try fixture.item("\(card)/comments/.trash/\(CommentIdent.two)", commentText())
_ = try reopen(window, fixture)
#expect(fixture.exists("\(card)/comments/.trash/\(CommentIdent.one)"), "the survivor is owned")
#expect(!fixture.exists("\(card)/comments/.trash/\(CommentIdent.two)"), "the crash leftover is not")
}
@Test("The board session's end purges a skip's survivor too")
func theBoardSessionsEndPurgesASkipSurvivor() async throws {
// "A stale-skipped step's backing instead survives **to board-session end**" the deferral
// has a floor, and it is the same one every other hold has (`AppModel`'s teardown).
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let (window, card) = try await skippedOnAForeignEdit(fixture)
window.board.clear()
#expect(try fixture.entryNames("\(card)/comments/.trash").isEmpty)
}
@@ -668,7 +841,7 @@ struct CardSessionPurgeTests {
// gesture leaves history for good, and its retirement runs (13 Interaction with the trash).
let provider = NativeHistoryProvider()
let retirement = HistoryStep.Retirement {}
provider.register(HistoryStep(name: "Edit Card", retirement: retirement, undo: { _ in .applied }, redo: { _ in .applied }))
provider.register(HistoryStep(name: coarseStep, retirement: retirement, undo: { _ in .applied }, redo: { _ in .applied }))
#expect(retirement.isOwed)
provider.undo()
@@ -683,7 +856,7 @@ struct CardSessionPurgeTests {
var runs = 0
let retirement = HistoryStep.Retirement { runs += 1 }
let provider = NativeHistoryProvider()
provider.register(HistoryStep(name: "Edit Card", retirement: retirement, undo: { _ in .applied }, redo: { _ in .applied }))
provider.register(HistoryStep(name: coarseStep, retirement: retirement, undo: { _ in .applied }, redo: { _ in .applied }))
provider.clear()
provider.clear()