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
+177 -6
View File
@@ -15,6 +15,12 @@ import Testing
/// lock, and never hot-looping on a failure.
/// 4. **A paste normalizes at the boundary** the pasted card lands already tidy.
///
/// Since 2026-07-31 there is a gate in front of all four: **the board-root `.gitignore` is the noise
/// definition**, and a loose file matching it is not work at all (§ Rules "skipped, preserved
/// verbatim, logged, never relocated, never announced"). That is section 1b, between the detection
/// and the write, because it is a property of what counts as a defect rather than of what is done
/// about one.
///
/// Like every other write suite here these read back through the loader or through raw bytes, never
/// through a snapshot the store handed out: the claims are about the files. `WriterFixture`, `Ident`
/// and `Item` come from `WriterTestSupport.swift`; `FakePasteboard` and `ClipboardHarness` from
@@ -24,15 +30,17 @@ import Testing
/// A one-lane, one-card board, ready for whatever the test wants to leave beside `index.md`.
///
/// It carries a **current agent guide**, which is what any board the app has opened once looks like
/// (08-agent-integration.md The agent guide). Without it the store's own guide refresh which
/// runs on every successful reload, beside this file's relocation would write a `CLAUDE.md` on
/// the first reload and open a bracket of its own, and the bracket counts below would stop being
/// claims about the relocation.
private func makeCardBoard() throws -> WriterFixture {
/// It carries a **current agent guide** and the **seeded `.gitignore`**, which is what any board the
/// app has opened once looks like (08-agent-integration.md The agent guide; 06-history-undo.md
/// Repository hygiene). Without them the store's own scheduled heals the guide refresh and the
/// seed, which run on every successful reload beside this file's relocation would write those two
/// files on the first reload and open brackets of their own, and the bracket counts below would stop
/// being claims about the relocation.
private func makeCardBoard(gitignore: String = BoardWriter.gitignoreSeed) throws -> WriterFixture {
let fixture = try WriterFixture()
try fixture.item("", Item.board)
try fixture.file(AgentGuide.filename, Data(AgentGuide.content.utf8))
try fixture.file(IntegrityRules.gitignoreFileName, Data(gitignore.utf8))
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "Fix login"))
return fixture
@@ -221,6 +229,169 @@ struct LooseFileDetectionTests {
}
}
// MARK: - 1b. The noise gate (the board-root .gitignore)
/// **`.gitignore` is the noise gate** (01-storage-format.md § Fractal layout Rules, ruled
/// 2026-07-31): "a file matching the board-root `.gitignore` standard gitignore pattern semantics
/// against the board-relative path; nested `.gitignore` files are ordinary strays the heal never
/// consults keeps the ordinary stray posture: skipped, preserved verbatim, logged, never
/// relocated, never announced. The exclusion list is exactly that file, nothing hardcoded".
///
/// The pattern *semantics* are `GitignoreRulesTests`'; what this suite pins is the wiring which
/// file is read, which path it is matched against, and that a match makes the file a stray rather
/// than work.
@Suite("Loose files ▸ the .gitignore noise gate")
struct LooseFileNoiseGateTests {
@Test("A file matching the board's .gitignore is not a loose file")
func matchedFileIsNotWork() throws {
let fixture = try makeCardBoard(gitignore: "*.tmp\n")
defer { fixture.tearDown() }
try fixture.file("\(cardPath)/scratch.tmp", Data("noise".utf8))
#expect(try looseFiles(in: fixture).isEmpty)
}
/// The seed's own first line, doing its job on the file it was written for. `.DS_Store` is also
/// hidden and hidden entries are excluded for a structural reason of their own (a relocated
/// hidden file would land where `attachmentNames` can never list it) so this is the belt and
/// the braces, which is what the ruling asks for: the seed is why it is *noise*.
@Test("A .DS_Store beside a card's index.md is neither relocated nor announced")
@MainActor
func finderLitterIsSilent() throws {
let fixture = try makeCardBoard()
defer { fixture.tearDown() }
try fixture.file("\(cardPath)/.DS_Store", Data("finder".utf8))
let store = try BoardStore(rootURL: fixture.root)
store.relocateLooseCardFiles()
#expect(fixture.exists("\(cardPath)/.DS_Store"), "preserved verbatim, exactly where it was")
#expect(!fixture.exists("\(cardPath)/attachments"))
#expect(store.banners.losses.isEmpty, "never announced")
#expect(store.banners.oneShots.isEmpty)
}
@Test("A matched custom pattern is skipped, preserved and never announced")
@MainActor
func matchedCustomPatternIsSilent() throws {
let fixture = try makeCardBoard(gitignore: "*.tmp\nbuild/\n")
defer { fixture.tearDown() }
let noise = try fixture.file("\(cardPath)/scratch.tmp", Data("noise".utf8))
let before = try stat(noise)
let store = try BoardStore(rootURL: fixture.root)
store.relocateLooseCardFiles()
let after = try stat(noise)
#expect(after.bytes == before.bytes)
#expect(after.modified == before.modified, "not opened, not moved, not touched")
#expect(!fixture.exists("\(cardPath)/attachments"))
#expect(store.banners.losses.isEmpty)
}
@Test("An unmatched loose file still relocates, with its notice")
@MainActor
func unmatchedFileStillRelocates() throws {
let fixture = try makeCardBoard(gitignore: "*.tmp\n")
defer { fixture.tearDown() }
try fixture.file("\(cardPath)/scratch.tmp", Data("noise".utf8))
try fixture.file("\(cardPath)/notes.txt", Data("notes".utf8))
let store = try BoardStore(rootURL: fixture.root)
store.relocateLooseCardFiles()
#expect(try fixture.data("\(cardPath)/attachments/notes.txt") == Data("notes".utf8))
#expect(fixture.exists("\(cardPath)/scratch.tmp"))
// One file moved, so one file is named the noise is not in the sentence either.
#expect(store.banners.losses.map(\.message) == ["Moved 'notes.txt' into attachments — 'Fix login'"])
}
/// 06's escape hatch, working: "the escape hatch for wanting no exclusions is an *empty* file".
@Test("An empty .gitignore excludes nothing — everything loose relocates")
@MainActor
func emptyFileExcludesNothing() throws {
let fixture = try makeCardBoard(gitignore: "")
defer { fixture.tearDown() }
try fixture.file("\(cardPath)/scratch.tmp", Data("noise".utf8))
try fixture.file("\(cardPath)/notes.txt", Data("notes".utf8))
let store = try BoardStore(rootURL: fixture.root)
store.relocateLooseCardFiles()
#expect(try fixture.entryNames("\(cardPath)/attachments") == ["notes.txt", "scratch.tmp"])
#expect(store.banners.losses.map(\.message) == ["Moved 2 files into attachments — 'Fix login'"])
}
/// A board that has never been opened by this version has no gate at all, and that reads as "no
/// exclusions" the pre-ruling behaviour, and the same answer the empty file gives.
@Test("A board with no .gitignore excludes nothing")
func noFileExcludesNothing() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
try fixture.item(cardPath, Item.rich(order: "1024", title: "Fix login"))
try fixture.file("\(cardPath)/notes.txt", Data("notes".utf8))
#expect(try looseFiles(in: fixture).map(\.fileNames) == [["notes.txt"]])
}
/// **The board-relative path is what patterns match**, which is what makes an anchored pattern
/// mean the board root rather than every card in the board.
@Test("Patterns match the board-relative path, so anchoring means the board root")
func anchoredPatternsMeanTheBoardRoot() throws {
let fixture = try makeCardBoard(gitignore: "/notes.txt\n")
defer { fixture.tearDown() }
try fixture.file("\(cardPath)/notes.txt", Data("notes".utf8))
// The pattern is about `<root>/notes.txt`; the card's file is a different path entirely.
#expect(try looseFiles(in: fixture).map(\.fileNames) == [["notes.txt"]])
// The same file, named the way the card actually sits on disk, is excluded.
let anchored = try makeCardBoard(gitignore: "/\(cardPath)/notes.txt\n")
defer { anchored.tearDown() }
try anchored.file("\(cardPath)/notes.txt", Data("notes".utf8))
#expect(try looseFiles(in: anchored).isEmpty)
}
/// "Nested `.gitignore` files are ordinary strays the heal never consults." One inside a card is
/// itself a hidden entry, so it is not even relocatable; one inside a *lane* is a lane-level
/// stray, and neither has any say over the card beside it.
@Test("Nested .gitignore files are never consulted")
func nestedFilesAreOrdinaryStrays() throws {
let fixture = try makeCardBoard(gitignore: "")
defer { fixture.tearDown() }
try fixture.file("\(Ident.lane1)/\(IntegrityRules.gitignoreFileName)", Data("*.txt\n".utf8))
try fixture.file("\(cardPath)/\(IntegrityRules.gitignoreFileName)", Data("*.txt\n".utf8))
try fixture.file("\(cardPath)/notes.txt", Data("notes".utf8))
#expect(try looseFiles(in: fixture).map(\.fileNames) == [["notes.txt"]])
#expect(fixture.exists("\(Ident.lane1)/\(IntegrityRules.gitignoreFileName)"))
#expect(fixture.exists("\(cardPath)/\(IntegrityRules.gitignoreFileName)"))
}
/// The import boundary reads the same file: a paste that swept a file the very next walk would
/// have left alone would be the app disagreeing with itself one gesture apart.
@Test("The import-boundary normalization obeys the same gate")
func normalizationObeysTheGate() throws {
let fixture = try makeCardBoard(gitignore: "*.tmp\n")
defer { fixture.tearDown() }
try fixture.file("\(cardPath)/scratch.tmp", Data("noise".utf8))
try fixture.file("\(cardPath)/notes.txt", Data("notes".utf8))
let moved = try BoardWriter.normalizeLooseFiles(inCard: cardFolder(in: fixture))
#expect(moved.map(\.fileName) == ["notes.txt"])
#expect(fixture.exists("\(cardPath)/scratch.tmp"))
// and the lane-level face, which reads the file once for the whole lane.
try fixture.file("\(cardPath)/second.tmp", Data("more noise".utf8))
#expect(try BoardWriter.normalizeLooseFiles(inLane: fixture.url(Ident.lane1)).isEmpty)
#expect(fixture.exists("\(cardPath)/second.tmp"))
}
}
// MARK: - 2. The write (BoardWriter)
@Suite("Loose files ▸ the relocation write")