Realign code with the 2026-07-29 findings-resolution rulings
Nine rulings land as code. Reorders don't stamp — one container-change predicate (WriteOperation.rewritesOrderOnly): within-container reorders and the renumber rescale rewrite only order, while cross-lane, cross-board, and trash moves stamp modified and clear modified-by; no trash special case exists, and the m8 undo inverses conform through the same seam. Copies are transactions: the root-strict/nested-lenient split retires for a whole-subtree stampability preflight that refuses loudly naming the offender, and every item-level copy severs remote/remote-state at every level (whole-board forks carry them verbatim). Paste refuses, never degrades: the embedded-index.md materialization and its loss row retire; a missing staged snapshot produces nothing and posts an error-tone one-shot named from manifest metadata. Coerce-tier fallbacks log through the Defect stream with path context attached loader-side. Displacement is level-uniform: a file squatting attachments inside a card heals by the same rename ladder as board-root squatters; comments stays tolerated. Delete Immediately joins card and lane context menus as Delete's ⌥-alternate with its own VO custom action, routed through an explicit container so the menu target outranks standing selection. Agent guide v7 teaches the stamp discipline and the card-level attachments claim, and sheds two stale v6 lines (lanes trash now; kind is taught). Verified conformant, unchanged: edition-aware Undo/Redo disable, trash marquee full-height backdrop. Both schemes 1854 tests / 318 suites green; verify-editions 30/30. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -528,6 +528,156 @@ struct TemplateEngineAtomicityTests {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The copy contract
|
||||
|
||||
/// **Instantiation is a copy transaction, and it severs tracker identity** — the two 2026-07-29
|
||||
/// rulings applied to the flow 01 names alongside paste and the ⌥-drag duplicate (01-storage-format.md
|
||||
/// § Frontmatter's compound-operations clause; § Fractal layout ▸ Rules' item-level sever).
|
||||
@Suite("TemplateEngine — the copy contract")
|
||||
struct TemplateEngineCopyContractTests {
|
||||
|
||||
/// A template carrying a readable-but-uneditable card refuses the **whole** create, naming that
|
||||
/// card, and leaves nothing where the user pointed — the former root-strict/descendants-lenient
|
||||
/// split would have made a board from it with one silently unstamped card inside.
|
||||
@Test("An uneditable card in the template refuses the create, naming it")
|
||||
func anUneditableCardRefusesTheCreate() throws {
|
||||
let template = try FixtureTemplate()
|
||||
defer { template.tearDown() }
|
||||
try template.fixture.item(
|
||||
"\(FixtureTemplate.name)/\(Ident.lane1)/\(Ident.card3)",
|
||||
Item.uneditable
|
||||
)
|
||||
let destination = template.destination()
|
||||
|
||||
let failure = instantiationFailure {
|
||||
try TemplateEngine.instantiate(template: try template.template(), to: destination, title: "Doomed")
|
||||
}
|
||||
guard case let .failed(write) = failure else {
|
||||
Issue.record("expected an ordinary failure, got \(String(describing: failure))")
|
||||
return
|
||||
}
|
||||
if case .uneditableFrontmatter = write.reason {} else {
|
||||
Issue.record("expected the uneditable-shape refusal, got \(write.reason)")
|
||||
}
|
||||
#expect(write.operation == .createBoard, "the create is what refused")
|
||||
#expect(
|
||||
!FileManager.default.fileExists(atPath: destination.path),
|
||||
"construct-then-clean: the partial destination goes with the refusal"
|
||||
)
|
||||
}
|
||||
|
||||
/// A template whose `.trash/` holds a broken card still instantiates: the preflight runs on the
|
||||
/// **destination**, after the copy applied its exclusions, so a card that was never going to be
|
||||
/// copied cannot refuse the create it has nothing to do with.
|
||||
@Test("An uneditable card in the template's trash refuses nothing — it is never copied")
|
||||
func anUneditableTrashCardIsIrrelevant() throws {
|
||||
let template = try FixtureTemplate()
|
||||
defer { template.tearDown() }
|
||||
try template.fixture.item("\(FixtureTemplate.name)/.trash/\(Ident.card3)", Item.uneditable)
|
||||
let destination = template.destination()
|
||||
|
||||
try TemplateEngine.instantiate(template: try template.template(), to: destination, title: "Fine")
|
||||
|
||||
#expect(FileManager.default.fileExists(atPath: destination.appendingPathComponent("index.md").path))
|
||||
#expect(
|
||||
!FileManager.default.fileExists(atPath: destination.appendingPathComponent(".trash").path),
|
||||
"and the trash was excluded, as always"
|
||||
)
|
||||
}
|
||||
|
||||
/// **The tracker sever, at every level an instantiation materializes** — board root, lane, and card.
|
||||
/// A template can carry the keys in from the board it was saved from (Save as Template is a fork and
|
||||
/// keeps them verbatim), and the board born from it must not claim those remote objects.
|
||||
@Test("Instantiation drops the reserved tracker keys at every level")
|
||||
func instantiationSeversTrackerIdentity() throws {
|
||||
let fixture = try WriterFixture()
|
||||
defer { fixture.tearDown() }
|
||||
let name = "Tracked.kanban"
|
||||
try fixture.item(name, """
|
||||
---
|
||||
schema: 1
|
||||
title: Tracked Template
|
||||
template: {order: 1}
|
||||
project: lanework
|
||||
remote: gitea#7
|
||||
---
|
||||
Blurb.
|
||||
|
||||
""")
|
||||
try fixture.item("\(name)/\(Ident.lane1)", Item.tracked(order: "1024", title: "To Do", key: "remote-state"))
|
||||
try fixture.item(
|
||||
"\(name)/\(Ident.lane1)/\(Ident.card1)",
|
||||
Item.tracked(order: "1024", title: "Starter", key: "remote")
|
||||
)
|
||||
|
||||
let template: BoardTemplate = switch TemplateEngine.load(templateAt: fixture.url(name), origin: .user) {
|
||||
case let .success(loaded): loaded
|
||||
case let .failure(error): throw error
|
||||
}
|
||||
let destination = fixture.url("Born.kanban")
|
||||
try TemplateEngine.instantiate(template: template, to: destination, title: "Born")
|
||||
|
||||
let board = try FrontmatterDocument.parse(String(
|
||||
decoding: Data(contentsOf: destination.appendingPathComponent("index.md")), as: UTF8.self
|
||||
))
|
||||
#expect(board.value(for: "remote") == nil, "the board born today claims no remote object")
|
||||
#expect(board.value(for: "project") != nil, "and every other unknown key rode along")
|
||||
|
||||
let lanes = ((try? BoardLoader.directoryCandidates(in: destination)) ?? [])
|
||||
.filter { BoardLoader.isUUIDShaped($0.lastPathComponent) }
|
||||
let lane = try FrontmatterDocument.parse(String(
|
||||
decoding: Data(contentsOf: try #require(lanes.first).appendingPathComponent("index.md")), as: UTF8.self
|
||||
))
|
||||
#expect(lane.value(for: "remote-state") == nil)
|
||||
|
||||
let card = try FrontmatterDocument.parse(String(
|
||||
decoding: Data(contentsOf: try #require(cardFolders(under: destination).first)
|
||||
.appendingPathComponent("index.md")), as: UTF8.self
|
||||
))
|
||||
#expect(card.value(for: "remote") == nil)
|
||||
#expect(card.value(for: "project") != nil)
|
||||
}
|
||||
|
||||
/// **Save as Template is a whole-board fork and is exempt** (01 ▸ Identity lifecycle's carve-out):
|
||||
/// it "carries them verbatim", GUIDs, timestamps and tracker keys alike, because a fork is a new
|
||||
/// namespace rather than a second claimant inside one board. The sever belongs to *item-level*
|
||||
/// copies, and this is the line between them.
|
||||
@Test("Save as Template carries the tracker keys verbatim")
|
||||
func saveAsTemplateIsExempt() throws {
|
||||
let fixture = try WriterFixture()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.item("Board.kanban", """
|
||||
---
|
||||
schema: 1
|
||||
title: Live Board
|
||||
remote: gitea#7
|
||||
---
|
||||
Body.
|
||||
|
||||
""")
|
||||
try fixture.item("Board.kanban/\(Ident.lane1)", Item.tracked(order: "1024", title: "To Do", key: "remote-state"))
|
||||
try fixture.item(
|
||||
"Board.kanban/\(Ident.lane1)/\(Ident.card1)",
|
||||
Item.tracked(order: "1024", title: "Card", key: "remote")
|
||||
)
|
||||
let store = fixture.url("Store")
|
||||
|
||||
let saved = try TemplateEngine.saveAsTemplate(
|
||||
boardAt: fixture.url("Board.kanban"), titled: "Live Board", into: store
|
||||
)
|
||||
|
||||
let board = try FrontmatterDocument.parse(String(
|
||||
decoding: Data(contentsOf: saved.appendingPathComponent("index.md")), as: UTF8.self
|
||||
))
|
||||
#expect(board.value(for: "remote") != nil, "a fork carries them verbatim")
|
||||
let lane = try FrontmatterDocument.parse(String(
|
||||
decoding: Data(contentsOf: saved.appendingPathComponent(Ident.lane1).appendingPathComponent("index.md")),
|
||||
as: UTF8.self
|
||||
))
|
||||
#expect(lane.value(for: "remote-state") != nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Shared
|
||||
|
||||
/// Every card folder under an instantiated board — `<root>/<lane>/<card>`, by the loader's own level
|
||||
|
||||
Reference in New Issue
Block a user