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:
@@ -303,7 +303,8 @@ struct BoardLoaderNonUUIDStrayTests {
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(realLane, "schema: 1\norder: 1024\n")
|
||||
// Missing 'order' — would be a fail-fast .missingOrder if this were UUID-shaped.
|
||||
// A hand-authored lane with a name that isn't identity-shaped: never a candidate, so its
|
||||
// contents are never read at all.
|
||||
try fixture.index("todo", "schema: 1\ntitle: Broken hand-authored lane\n")
|
||||
|
||||
let result = try BoardLoader.load(boardRoot: fixture.root)
|
||||
@@ -725,54 +726,25 @@ struct BoardLoaderFailFastTests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A UUID-shaped folder still fails fast on structurally-bad content — the name shape only
|
||||
/// gates *candidacy*, never the validity of a folder that qualifies.
|
||||
@Test func missingOrderOnUUIDLaneThrows() throws {
|
||||
/// A schema newer than the app fails fast **below** the root too — the one `schema` rule the
|
||||
/// optional-key ruling left alone (01-storage-format.md § Malformed input, re-ruled 2026-07-31).
|
||||
@Test func schemaNewerThanAppOnALaneThrows() throws {
|
||||
let fixture = try BoardFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
let lane = uuidFolderName()
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(lane, "schema: 1\n")
|
||||
try fixture.index(lane, "schema: 2\norder: 1024\n")
|
||||
|
||||
expectFailure(.missingOrder, path: "\(lane)/index.md") {
|
||||
expectFailure(.schemaNewerThanApp(found: 2), path: "\(lane)/index.md") {
|
||||
_ = try BoardLoader.load(boardRoot: fixture.root)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func malformedOrderOnUUIDLaneThrows() throws {
|
||||
let fixture = try BoardFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
let lane = uuidFolderName()
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(lane, "schema: 1\norder: not-a-number\n")
|
||||
|
||||
expectFailure(.malformedOrder(raw: "not-a-number"), path: "\(lane)/index.md") {
|
||||
_ = try BoardLoader.load(boardRoot: fixture.root)
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-finite `order` (`.nan`, `.inf`) is the same loud rejection as a non-numeric one
|
||||
/// (01-storage-format.md § Frontmatter, settled) — NaN has no place in the total order the
|
||||
/// tie-break and midpoint math assume.
|
||||
@Test func nonFiniteOrderOnUUIDLaneThrows() throws {
|
||||
let fixture = try BoardFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
let lane = uuidFolderName()
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(lane, "schema: 1\norder: .nan\n")
|
||||
|
||||
expectFailure(.malformedOrder(raw: ".nan"), path: "\(lane)/index.md") {
|
||||
_ = try BoardLoader.load(boardRoot: fixture.root)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func missingOrderOnUUIDCardThrows() throws {
|
||||
/// A malformed `schema` below the root still refuses: the ruling made the *absent* key optional,
|
||||
/// not the unreadable one — reading `schema: one` as 1 would be inventing agreement.
|
||||
@Test func malformedSchemaOnACardThrows() throws {
|
||||
let fixture = try BoardFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
@@ -781,14 +753,162 @@ struct BoardLoaderFailFastTests {
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(lane, "schema: 1\norder: 1024\n")
|
||||
try fixture.index("\(lane)/\(card)", "schema: 1\n")
|
||||
try fixture.index("\(lane)/\(card)", "schema: one\norder: 1024\n")
|
||||
|
||||
expectFailure(.missingOrder, path: "\(lane)/\(card)/index.md") {
|
||||
expectFailure(.malformedSchema(raw: "one"), path: "\(lane)/\(card)/index.md") {
|
||||
_ = try BoardLoader.load(boardRoot: fixture.root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - `order` and `schema` optional below the board root
|
||||
|
||||
/// **The append-at-end reading** (01-storage-format.md § Ordering, re-ruled 2026-07-31): below the
|
||||
/// board root a missing, null, non-numeric or non-finite `order` is no longer a fail-fast — it reads
|
||||
/// as a rank past every ordered sibling, tie-broken by folder name, and the reading is coerce-tier
|
||||
/// (logged, bytes preserved). `Fixtures/Valid/optional-keys.kanban` is the disk-backed golden case;
|
||||
/// these are the synthetic edges.
|
||||
struct BoardLoaderOptionalOrderTests {
|
||||
|
||||
/// The zero-read minimum the ruling exists for: a lane with one ranked card and one card whose
|
||||
/// whole frontmatter is a title.
|
||||
@Test func orderlessCardAppendsAfterEveryRankedSibling() throws {
|
||||
let fixture = try BoardFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
let lane = "10000000-0000-4000-8000-000000000001"
|
||||
let ranked = "20000000-0000-4000-8000-000000000001"
|
||||
// Deliberately the *lower* folder name, so folder order alone would put it first.
|
||||
let orderless = "10000000-0000-4000-8000-000000000009"
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(lane, "schema: 1\norder: 1024\n")
|
||||
try fixture.index("\(lane)/\(ranked)", "schema: 1\norder: 4096\n")
|
||||
try fixture.index("\(lane)/\(orderless)", "title: Minimum\n")
|
||||
|
||||
let model = try BoardLoader.load(boardRoot: fixture.root).model
|
||||
#expect(model.lanes[0].cards.map(\.id.rawValue) == [ranked, orderless])
|
||||
#expect(model.lanes[0].cards.map(\.order) == [4096, 5120])
|
||||
}
|
||||
|
||||
/// Two order-less siblings: folder name decides, and the ranks they read as are `append`'s own
|
||||
/// ladder — which is what lets the Writer stamp them without anything moving.
|
||||
@Test func twoOrderlessSiblingsSortByFolderName() throws {
|
||||
let fixture = try BoardFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
let lane = "10000000-0000-4000-8000-000000000001"
|
||||
let second = "30000000-0000-4000-8000-000000000002"
|
||||
let first = "20000000-0000-4000-8000-000000000001"
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(lane, "schema: 1\norder: 1024\n")
|
||||
try fixture.index("\(lane)/\(second)", "schema: 1\n")
|
||||
try fixture.index("\(lane)/\(first)", "schema: 1\n")
|
||||
|
||||
let model = try BoardLoader.load(boardRoot: fixture.root).model
|
||||
#expect(model.lanes[0].cards.map(\.id.rawValue) == [first, second])
|
||||
// No ranked sibling at all, so the ladder bases at 0 — the empty-container convention.
|
||||
#expect(model.lanes[0].cards.map(\.order) == [1024, 2048])
|
||||
}
|
||||
|
||||
/// The four unusable shapes are one reading. Each is a coercion carrying the text as written.
|
||||
@Test func everyUnusableOrderShapeReadsAsAppendAtEnd() throws {
|
||||
let fixture = try BoardFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
let lane = "10000000-0000-4000-8000-000000000001"
|
||||
let anchor = "20000000-0000-4000-8000-000000000000"
|
||||
let shapes: [(id: String, frontmatter: String, raw: String)] = [
|
||||
("20000000-0000-4000-8000-000000000001", "schema: 1\n", ""),
|
||||
("20000000-0000-4000-8000-000000000002", "schema: 1\norder:\n", ""),
|
||||
("20000000-0000-4000-8000-000000000003", "schema: 1\norder: null\n", "null"),
|
||||
("20000000-0000-4000-8000-000000000004", "schema: 1\norder: banana\n", "banana"),
|
||||
("20000000-0000-4000-8000-000000000005", "schema: 1\norder: .nan\n", ".nan"),
|
||||
("20000000-0000-4000-8000-000000000006", "schema: 1\norder: .inf\n", ".inf"),
|
||||
]
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(lane, "schema: 1\norder: 1024\n")
|
||||
try fixture.index("\(lane)/\(anchor)", "schema: 1\norder: 2048\n")
|
||||
for shape in shapes {
|
||||
try fixture.index("\(lane)/\(shape.id)", shape.frontmatter)
|
||||
}
|
||||
|
||||
let result = try BoardLoader.load(boardRoot: fixture.root)
|
||||
#expect(result.model.lanes[0].cards.map(\.id.rawValue) == [anchor] + shapes.map(\.id))
|
||||
#expect(result.model.lanes[0].cards.map(\.order) == [2048, 3072, 4096, 5120, 6144, 7168, 8192])
|
||||
|
||||
let coerced = Dictionary(
|
||||
uniqueKeysWithValues: result.coercedFrontmatter.map { ($0.path, $0.fields) })
|
||||
for shape in shapes {
|
||||
#expect(
|
||||
coerced["\(lane)/\(shape.id)/index.md"] == [CoercedField(key: "order", raw: shape.raw)],
|
||||
"\(shape.frontmatter) should coerce with raw '\(shape.raw)'"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The rule holds one level up: an order-less lane sits right of every ranked one.
|
||||
@Test func orderlessLaneAppendsAtTheEndOfTheStrip() throws {
|
||||
let fixture = try BoardFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
let ranked = "90000000-0000-4000-8000-000000000001"
|
||||
let orderless = "10000000-0000-4000-8000-000000000002"
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(ranked, "schema: 1\norder: 1024\n")
|
||||
try fixture.index(orderless, "schema: 1\n")
|
||||
|
||||
let model = try BoardLoader.load(boardRoot: fixture.root).model
|
||||
#expect(model.lanes.map(\.id.rawValue) == [ranked, orderless])
|
||||
#expect(model.lanes.map(\.order) == [1024, 2048])
|
||||
}
|
||||
|
||||
/// A missing `schema` below the root reads as 1 and records a coercion; the **root's** own
|
||||
/// missing `schema` is still the loud rejection (`missingSchemaThrows` above).
|
||||
@Test func missingSchemaBelowRootReadsAsOne() throws {
|
||||
let fixture = try BoardFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
let lane = uuidFolderName()
|
||||
let card = uuidFolderName()
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(lane, "order: 1024\ntitle: No Schema Lane\n")
|
||||
try fixture.index("\(lane)/\(card)", "order: 1024\ntitle: No Schema Card\n")
|
||||
|
||||
let result = try BoardLoader.load(boardRoot: fixture.root)
|
||||
#expect(result.model.lanes[0].schema == 1)
|
||||
#expect(result.model.lanes[0].cards[0].schema == 1)
|
||||
|
||||
let paths = Set(result.coercedFrontmatter.map(\.path))
|
||||
#expect(paths == ["\(lane)/index.md", "\(lane)/\(card)/index.md"])
|
||||
#expect(result.coercedFrontmatter.allSatisfy { $0.fields == [CoercedField(key: "schema", raw: "")] })
|
||||
}
|
||||
|
||||
/// A trash entry without a rank reads like every other order-less file. `order` decides nothing
|
||||
/// about where a trash row sits — `modified` does — so this is only about the rank it carries
|
||||
/// back out on a restore.
|
||||
@Test func orderlessTrashEntryReadsAsAppendAtEnd() throws {
|
||||
let fixture = try BoardFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
let ranked = "20000000-0000-4000-8000-000000000001"
|
||||
let orderless = "10000000-0000-4000-8000-000000000002"
|
||||
|
||||
try fixture.index("", "schema: 1\n")
|
||||
try fixture.index(".trash/\(ranked)", "schema: 1\nkind: card\norder: 1024\n")
|
||||
try fixture.index(".trash/\(orderless)", "schema: 1\nkind: card\n")
|
||||
|
||||
let model = try BoardLoader.load(boardRoot: fixture.root).model
|
||||
let byID = Dictionary(uniqueKeysWithValues: model.trash.map { ($0.id.rawValue, $0.order) })
|
||||
#expect(byID[ranked] == 1024)
|
||||
#expect(byID[orderless] == 2048)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Encoding strictness
|
||||
|
||||
/// The loader decodes byte-faithfully (no NSString BOM-stripping) so the settled encoding
|
||||
|
||||
Reference in New Issue
Block a user