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