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
+219 -11
View File
@@ -4,9 +4,10 @@ import Testing
import libgit2
@testable import Kanban
/// **Repository hygiene** (06-history-undo.md Repository hygiene) the two behaviours that keep a
/// git board's `.git` sane without ever rewriting anything: the `.gitignore` seeded once at init, and
/// the periodic repack that packs loose objects and touches nothing else.
/// **Repository hygiene** (06-history-undo.md Repository hygiene) the behaviours that keep a
/// board's noise out of the way without ever rewriting anything: the `.gitignore` **every board**
/// carries (re-ruled 2026-07-31 the file outgrew git, so it is seeded at creation and healed in at
/// open, git or not), and the periodic repack that packs loose objects and touches nothing else.
///
/// Every repository here is a **real** one, made by the app's own add-git through the bundled
/// libgit2, and every assertion is read off the filesystem or out of the object database rather than
@@ -154,11 +155,16 @@ private func historyWalk(at boardRoot: URL) throws -> [String] {
// MARK: - .gitignore seeding
/// **The add-git half.** Since 2026-07-31 the seed belongs to the *board* rather than to git (the
/// suite below this one), and what survives here is the last-chance check in front of the initial
/// commit: whatever else happened, the tree that becomes "Initial board state" carries a
/// `.gitignore`, because a `.DS_Store` that enters history can never be got out again (06 Deleting
/// never forgets).
@MainActor
@Suite("Repository hygiene ▸ the seeded .gitignore")
struct GitignoreSeedTests {
@Test("Add-git seeds a .gitignore containing .DS_Store, inside the initial commit")
@Test("Add-git guarantees a .gitignore inside the initial commit")
func addGitSeedsTheIgnoreFile() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
@@ -166,9 +172,9 @@ struct GitignoreSeedTests {
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
#expect(await git.addGit())
// The file, and the whole of the file: one line, because one line is the rule
// (06 Repository hygiene: "a minimal `.gitignore` (`.DS_Store`)").
#expect(try fixture.data(".gitignore") == Data(".DS_Store\n".utf8))
// The file, and the whole of the file the one seed text, shared with board creation and
// the open-time heal (06 Repository hygiene: "`.DS_Store` plus the writer's temp pattern").
#expect(try fixture.data(".gitignore") == Data(BoardWriter.gitignoreSeed.utf8))
// **In "Initial board state", not after it.** Seeding after the commit would put the app's
// own file into the board's first *foreign* commit; seeding before makes it part of the
@@ -236,7 +242,34 @@ struct GitignoreSeedTests {
#expect(try snapshot(fixture.root, ".gitignore") == before)
}
@Test("Adoption seeds nothing — an adopted repository is somebody else's init")
/// **The second consumer of the one noise definition** (01-storage-format.md § Fractal layout
/// Rules: "On Pro boards the same file governs the committer, so ignored noise neither relocates
/// nor commits one definition of noise, two consumers"). The committer's own condition is
/// `changedPaths`, which stages through libgit2 with ignores respected; this pins that the file
/// the loose-file gate reads is the file that decides what commits.
@Test("The committer obeys the same file — ignored noise never becomes a changed path")
func theCommitterObeysTheSameFile() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
#expect(await git.addGit())
// The user fine-tunes their own noise definition, which is exactly what the file is for.
try fixture.file(".gitignore", Data((BoardWriter.gitignoreSeed + "*.tmp\n").utf8))
try fixture.file("\(Ident.lane1)/\(Ident.card1)/scratch.tmp", Data("noise".utf8))
try fixture.file("\(Ident.lane1)/notes.txt", Data("a real stray".utf8))
try fixture.file("\(Ident.lane1)/.DS_Store", Data([0x00, 0x01, 0x42]))
let changed = GitCommitOperation.changedPaths(at: fixture.root).map(\.path)
#expect(!changed.contains { $0.hasSuffix("scratch.tmp") })
#expect(!changed.contains { $0.hasSuffix(".DS_Store") })
#expect(changed.contains { $0.hasSuffix("notes.txt") }, "and an ordinary stray still commits")
}
/// Composing history over somebody else's repository writes nothing at all adoption is not an
/// init, and no *git* path seeds. (The board's own heal is what gives such a board its
/// `.gitignore`, at open, and it is exercised in the suite below.)
@Test("Adoption writes nothing — an adopted repository is somebody else's init")
func adoptionSeedsNothing() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
@@ -245,11 +278,14 @@ struct GitignoreSeedTests {
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
#expect(git.mode == .git)
#expect(!fixture.exists(".gitignore"), "the seed belongs to the app's own init and nowhere else")
#expect(!fixture.exists(".gitignore"), "composing history is not a write")
}
@Test("A repo-nested board gets no seed, because it gets no app-managed git")
func repoNestedBoardsGetNothing() async throws {
/// A repo-nested board gets no *git* of the app's, so no git path can seed it and the
/// enclosing repository is never written into either. What such a board does get is the ordinary
/// board-level seed at open (06's "Repo-nested boards are seeded too"), which is the suite below.
@Test("The git paths never touch a repo-nested board, or its enclosing repo")
func repoNestedBoardsGetNothingFromGit() async throws {
let outer = try WriterFixture()
defer { outer.tearDown() }
try outer.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8))
@@ -266,6 +302,178 @@ struct GitignoreSeedTests {
}
}
// MARK: - The .gitignore every board carries
/// **"`.gitignore` seeded on every board, never touched after"** (06-history-undo.md Repository
/// hygiene, re-ruled 2026-07-31 "the file outgrew git: it is the one noise definition the
/// loose-file relocation heal obeys so every board carries it, git or not").
///
/// Three claims, and they are the whole ruling: **creation writes it**, **a board missing it gains
/// it by scheduled heal at open**, and **the app never edits an existing one** an empty file
/// included, which is the ruling's own escape hatch. The gate it feeds is
/// `LooseFileRelocationTests` the noise gate; the pattern semantics are `GitignoreRulesTests`.
@MainActor
@Suite("Repository hygiene ▸ the .gitignore every board carries")
struct BoardGitignoreSeedTests {
private func seedURL(in fixture: WriterFixture) -> URL {
fixture.root.appendingPathComponent(IntegrityRules.gitignoreFileName)
}
private func stat(_ url: URL) throws -> (bytes: Data, modified: Date) {
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
guard let modified = attributes[.modificationDate] as? Date else {
throw NSError(domain: "BoardGitignoreSeedTests", code: 1)
}
return (try Data(contentsOf: url), modified)
}
@Test("Board creation writes the seed beside index.md")
func creationSeeds() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let root = fixture.url("New Board.kanban")
try BoardWriter.createBoard(at: root, title: "New Board")
#expect(try Data(contentsOf: root.appendingPathComponent(IntegrityRules.gitignoreFileName))
== Data(BoardWriter.gitignoreSeed.utf8))
}
@Test("A board missing the file gains it at open, silently")
func healSeedsAtOpen() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try fixture.file(AgentGuide.filename, Data(AgentGuide.content.utf8))
#expect(!fixture.exists(IntegrityRules.gitignoreFileName))
let store = try BoardStore(rootURL: fixture.root)
store.runScheduledHeals()
#expect(try fixture.data(IntegrityRules.gitignoreFileName) == Data(BoardWriter.gitignoreSeed.utf8))
// A courtesy file the user did not create and may not know exists the guide's posture.
#expect(store.banners.losses.isEmpty)
#expect(store.banners.oneShots.isEmpty)
}
/// The heal's memo, doing its two jobs: a picture already acted on is not acted on again (no
/// second write), and a picture that comes *back* a foreign deletion heals again, because the
/// memo was cleared on success.
@Test("Seeding twice writes once, and a deleted file comes back")
func memoIsArmedAndCleared() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try fixture.file(AgentGuide.filename, Data(AgentGuide.content.utf8))
let store = try BoardStore(rootURL: fixture.root)
store.seedGitignore()
let first = try stat(seedURL(in: fixture))
#expect(store.heals.memo(for: .missingGitignore) == nil, "cleared on success")
store.seedGitignore()
#expect(try stat(seedURL(in: fixture)) == first, "not rewritten — not even opened")
// What a foreign deletion looks like: the picture "missing" is restored, and a standing memo
// would have made that deletion the one thing this could not heal.
try FileManager.default.removeItem(at: seedURL(in: fixture))
store.seedGitignore()
#expect(try fixture.data(IntegrityRules.gitignoreFileName) == Data(BoardWriter.gitignoreSeed.utf8))
}
@Test("An existing .gitignore is left byte-for-byte alone, mtime included")
func existingFileIsNeverRewritten() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try fixture.file(AgentGuide.filename, Data(AgentGuide.content.utf8))
let theirs = Data("# mine\nbuild/\n*.tmp\n".utf8)
try fixture.file(IntegrityRules.gitignoreFileName, theirs)
let before = try stat(seedURL(in: fixture))
let store = try BoardStore(rootURL: fixture.root)
store.runScheduledHeals()
#expect(try fixture.data(IntegrityRules.gitignoreFileName) == theirs)
#expect(try stat(seedURL(in: fixture)) == before, "never merged, never appended to, never opened")
}
/// "The escape hatch for wanting no exclusions is an *empty* file, which the app honors and never
/// rewrites" the one case where re-seeding would look most reasonable and is most wrong.
@Test("An empty .gitignore is honored and never rewritten")
func emptyFileIsHonored() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try fixture.file(AgentGuide.filename, Data(AgentGuide.content.utf8))
try fixture.file(IntegrityRules.gitignoreFileName, Data())
let before = try stat(seedURL(in: fixture))
let store = try BoardStore(rootURL: fixture.root)
store.runScheduledHeals()
store.runScheduledHeals()
#expect(try fixture.data(IntegrityRules.gitignoreFileName) == Data())
#expect(try stat(seedURL(in: fixture)) == before)
}
/// "Repo-nested boards are seeded too (re-ruling the old no-app-`.gitignore` posture): the file
/// serves the heal there, not any app-managed git" so there is no repo-detection gate on this
/// heal, and the enclosing repository is still never written into.
@Test("A repo-nested board is seeded like any other")
func repoNestedBoardsAreSeeded() throws {
let outer = try WriterFixture()
defer { outer.tearDown() }
try outer.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8))
let boardRoot = outer.root.appendingPathComponent("board", isDirectory: true)
try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)
try Data(Item.board.utf8).write(to: boardRoot.appendingPathComponent("index.md"))
try Data(AgentGuide.content.utf8).write(to: boardRoot.appendingPathComponent(AgentGuide.filename))
let store = try BoardStore(rootURL: boardRoot)
store.runScheduledHeals()
#expect(try Data(contentsOf: boardRoot.appendingPathComponent(IntegrityRules.gitignoreFileName))
== Data(BoardWriter.gitignoreSeed.utf8))
#expect(!FileManager.default.fileExists(atPath: outer.root.appendingPathComponent(IntegrityRules.gitignoreFileName).path))
}
/// **The claimed name that does not displace** (`IntegrityRules.claimedRootNames`): a wrong-kind
/// node wearing `.gitignore` is left exactly where it is, because a board with no readable noise
/// definition simply excludes nothing nothing breaks while the name is held, so nothing of the
/// user's is moved to buy a courtesy file.
@Test("A folder wearing the name is left alone, and nothing is written through it")
func squatterIsLeftAlone() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try fixture.file(AgentGuide.filename, Data(AgentGuide.content.utf8))
try fixture.file("\(IntegrityRules.gitignoreFileName)/inside.txt", Data("mine".utf8))
let store = try BoardStore(rootURL: fixture.root)
store.runScheduledHeals()
#expect(try fixture.data("\(IntegrityRules.gitignoreFileName)/inside.txt") == Data("mine".utf8))
#expect(store.banners.oneShots.isEmpty, "and no failure is reported for work nobody asked for")
#expect(store.banners.losses.isEmpty)
}
/// A board whose location cannot be written to defers rather than failing the engine's gate,
/// stated here because this heal runs at every open of every board and is the one most likely to
/// meet a read-only volume.
@Test("An unwritable board root is skipped silently")
func unwritableRootIsSkipped() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try fixture.file(AgentGuide.filename, Data(AgentGuide.content.utf8))
let store = try BoardStore(rootURL: fixture.root)
try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: fixture.root.path)
defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fixture.root.path) }
store.seedGitignore()
#expect(!fixture.exists(IntegrityRules.gitignoreFileName))
#expect(store.banners.oneShots.isEmpty)
#expect(store.heals.memo(for: .missingGitignore) == nil, "deferred, never remembered")
}
}
// MARK: - The housekeeping pass
@MainActor