Build the integrity service - IntegrityRules and the HealScheduler

The 2026-07-29 integrity design pass, consolidated (DESIGN/01 -
Validation and healing; DESIGN/02 - Components): IntegrityRules
(Storage, pure) is the one home for the identity predicate and
canonical form (BoardWriter.canonicalIdentity deleted, ItemID and the
loader forward to it), the per-field rulebook, uneditable shapes,
per-kind index validation, the reserved-name tables, and the trash
kind discriminator (values trusted - kind: lane/card explicit,
unrecognized falls to shape). LoadResult's ad-hoc channels fold into
one typed Defect stream (looseCardFiles / legacyTombstone /
claimedNameSquatted, per-defect heal signatures); the old accessors
survive as computed views.

HealScheduler (LiveStore) states the six-step heal pattern once -
resting-clear, lock gate, isWritableFile gate (now covering all four
heals), signature memo armed-before-attempt with explicit
clear-on-success, disk re-verify in each write half, one banner-posture
table (BannerCenter keeps all phrasing). The three hand-rolled healers
run on it with behavior preserved - including the
relocation-notice-despite-partial-failure quirk, deliberately. Heals
run at the reload tail AND at registry acquire, closing the
migration-never-fires-at-open asymmetry. Displacement runs first: a
squatted .trash would otherwise fail the migration and arm its memo
against an unchanged picture.

Claimed-name squatters (ruled today, 62c47a2) displace by the shared
Finder-style rename ladder - preserved verbatim, symlinks moved as
links, nothing stamped; AgentGuide's untouchable-skip upgrades to
displace-then-write, the CLAUDE.user.md-taken skip stands. kind stamps
on every create and backfills on any index rewrite via the on-touch
seam (placement resolver stamps nothing when the parent is unknown -
a guessed kind is worse than an absent one; board-root writers declare
theirs). Heal writes mark their EchoLedger receipts (inert in base;
pro-m1's committer will split them into their own commits). The
renumber ask-renumber-ask-again two-step is one shared helper, adopted
at all nine call sites.

69 tests added. 1738 green on both schemes.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-29 15:45:48 -04:00
parent 0d846c634e
commit 3a9db2e78b
27 changed files with 3226 additions and 617 deletions
+168
View File
@@ -516,3 +516,171 @@ struct EchoLedgerStoreTests {
#expect(log.lines == ["Board changed: 1 lane edited"])
}
}
// MARK: - Heal-marked receipts
/// **The Writer's heal operations drop heal-marked receipts** (06-history-undo.md Commit messages,
/// ruled 2026-07-29: "attribution machinery like the author split, never message tagging").
///
/// **Inert in base beyond the ledger itself**: nothing here reads the flag and nothing renders it
/// it is what pro-m1's committer will read to split a heal's paths into their own commit, and these
/// tests pin the seam it will read from, not a committer that does not exist yet.
@Suite("EchoLedger — heal-marked receipts")
struct EchoLedgerHealMarkTests {
/// An ordinary gesture is not a heal, and says so by default.
@Test("An ordinary write is not heal-marked")
func ordinaryWritesAreNotHeals() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let ledger = EchoLedger()
let folder = fixture.url(lane1)
try EchoLedger.$current.withValue(ledger) {
try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
document.set(FrontmatterKeys.width, to: .int(3))
}
}
#expect(!ledger.isHeal(at: folder.appendingPathComponent("index.md")))
}
/// The loose-file relocation an app-initiated heal, so its landed file's receipt is marked.
@Test("The loose-file relocation marks what it moved")
func relocationMarksItsMoves() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let ledger = EchoLedger()
let cardFolder = fixture.url("\(lane1)/\(card1)")
try fixture.file("\(lane1)/\(card1)/notes.txt", Data("notes".utf8))
try EchoLedger.$current.withValue(ledger) {
_ = try BoardWriter.relocateLooseFiles(["notes.txt"], inCard: cardFolder)
}
#expect(ledger.isHeal(at: cardFolder.appendingPathComponent("attachments/notes.txt")))
}
/// **The import-boundary normalization is not marked**, and the distinction is the design's:
/// an *inline* heal batches with the gesture that triggered it, so its paths belong in that
/// gesture's commit rather than in a heal's own.
@Test("The paste boundary's normalization is not heal-marked")
func inlineNormalizationIsNotAHeal() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let ledger = EchoLedger()
let cardFolder = fixture.url("\(lane1)/\(card1)")
try fixture.file("\(lane1)/\(card1)/notes.txt", Data("notes".utf8))
try EchoLedger.$current.withValue(ledger) {
_ = try BoardWriter.normalizeLooseFiles(inCard: cardFolder)
}
#expect(!ledger.isHeal(at: cardFolder.appendingPathComponent("attachments/notes.txt")))
}
/// The legacy-tombstone migration marks both halves of what it wrote and the ordinary delete
/// it shares a body with does not, because that one is a gesture.
@Test("The tombstone migration marks, the delete beside it does not")
func migrationMarksButDeleteDoesNot() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let ledger = EchoLedger()
try fixture.item(
"\(lane1)/\(Ident.card3)",
"---\nschema: 1\norder: 4096\ntitle: Tombstoned\ndeleted: 2026-01-01T00:00:00Z\n---\nbody\n"
)
try EchoLedger.$current.withValue(ledger) {
_ = try BoardWriter.migrateTombstonedCard(
at: fixture.url("\(lane1)/\(Ident.card3)"),
inBoard: fixture.root,
order: 1024
)
_ = try BoardWriter.deleteCardToTrash(
at: fixture.url("\(lane1)/\(card1)"),
inBoard: fixture.root,
order: 2048
)
}
let migrated = fixture.url(".trash/\(Ident.card3)")
#expect(ledger.isHeal(at: migrated))
#expect(ledger.isHeal(at: migrated.appendingPathComponent("index.md")))
let deleted = fixture.url(".trash/\(card1)")
#expect(!ledger.isHeal(at: deleted))
#expect(!ledger.isHeal(at: deleted.appendingPathComponent("index.md")))
}
/// The agent guide's write, and the claimed-name displacement both app-initiated, both marked.
@Test("The guide write and a displacement are heals")
func guideAndDisplacementAreHeals() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let ledger = EchoLedger()
try Data("squatter".utf8).write(to: fixture.root.appendingPathComponent(".trash"))
try EchoLedger.$current.withValue(ledger) {
_ = try BoardWriter.displaceClaimedName(
ClaimedNameSquatter(name: ".trash", found: .file, expected: .directory),
atBoardRoot: fixture.root
)
_ = try AgentGuide.install(atBoardRoot: fixture.root)
}
#expect(ledger.isHeal(at: fixture.root.appendingPathComponent(".trash 2")))
#expect(ledger.isHeal(at: fixture.root.appendingPathComponent(AgentGuide.filename)))
}
/// **Supersession drops the mark with the receipt it belonged to**: a later ordinary write to a
/// healed path is exactly the case where the path stops being the heal's alone.
@Test("An ordinary write over a healed path clears the mark")
func supersessionClearsTheMark() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let ledger = EchoLedger()
let index = fixture.url(lane1).appendingPathComponent("index.md")
ledger.recordWrite(at: index, text: "one")
ledger.markHeal(at: index)
#expect(ledger.isHeal(at: index))
ledger.recordWrite(at: index, text: "two")
#expect(!ledger.isHeal(at: index))
#expect(ledger.receipt(at: index) == .content(hash: EchoLedger.hash(of: "two")))
}
/// A mark with no receipt to attach to describes nothing, so it creates nothing.
@Test("Marking an unknown path is a no-op")
func markingAnUnknownPathIsANoOp() {
let ledger = EchoLedger()
ledger.markHeal(atPath: "/nowhere/index.md")
#expect(ledger.receipt(atPath: "/nowhere/index.md") == nil)
#expect(!ledger.isHeal(atPath: "/nowhere/index.md"))
#expect(ledger.outstandingReceipts == 0)
}
/// A move is one fact under two keys, so marking either end marks both the same rule that
/// retires both ends when one is consumed.
@Test("Marking one end of a move marks both")
func markingAMoveMarksBothEnds() {
let ledger = EchoLedger()
ledger.recordMove(fromPath: "/b/lane/card", toPath: "/b/.trash/card")
ledger.markHeal(atPath: "/b/.trash/card")
#expect(ledger.isHeal(atPath: "/b/.trash/card"))
#expect(ledger.isHeal(atPath: "/b/lane/card"))
}
/// The flag changes **nothing** about classification: a heal is an app write like any other, and
/// the render path never consults the ledger at all.
@Test("A heal mark does not change provenance")
func markDoesNotChangeClassification() {
let ledger = EchoLedger()
let index = "/b/lane/index.md"
ledger.recordWrite(atPath: index, hash: EchoLedger.hash(of: "one"))
ledger.markHeal(atPath: index)
#expect(ledger.classify([index: .content(hash: EchoLedger.hash(of: "one"))]) == .appMediated)
}
}