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:
@@ -116,6 +116,13 @@ public enum IntegrityRules: Sendable {
|
||||
/// The file every level's content lives in.
|
||||
public static let indexFileName = "index.md"
|
||||
|
||||
/// **The board's noise definition** (01-storage-format.md § Fractal layout ▸ Rules, ruled
|
||||
/// 2026-07-31; 06-history-undo.md ▸ Repository hygiene) — a board-root **file** name, claimed
|
||||
/// because the app seeds it on every board and then reads it on every walk: it is the one list of
|
||||
/// files the loose-file relocation heal leaves alone, and on a Pro board the same file is what
|
||||
/// keeps that noise out of history. Seeded once, never edited afterwards.
|
||||
public static let gitignoreFileName = ".gitignore"
|
||||
|
||||
/// A card's **comment thread** (01-storage-format.md § Enhanced schema) — a plain reserved
|
||||
/// child, never a level and never identity; the identities are the UUID folders inside it.
|
||||
public static let commentsFolderName = "comments"
|
||||
@@ -180,11 +187,24 @@ public enum IntegrityRules: Sendable {
|
||||
/// Whether a wrong-kinded node on this name is displaced by the scheduled heal (ruled
|
||||
/// 2026-07-29 — "Lanework owns the board"), or left exactly where it is.
|
||||
///
|
||||
/// `false` for the two names that are *destinations* or not the app's to police:
|
||||
/// `false` for the two names that are *destinations* or that the app can do without:
|
||||
/// `CLAUDE.user.md` is where a markerless `CLAUDE.md` is rescued **to**, and freeing a
|
||||
/// destination by a second displacement would cascade renames (the settled skip stands —
|
||||
/// 08-agent-integration.md); `.gitignore` is seeded once and then the user's to edit
|
||||
/// (06-history-undo.md ▸ Repository hygiene), and nothing in the app reads it.
|
||||
/// (06-history-undo.md ▸ Repository hygiene).
|
||||
///
|
||||
/// **`.gitignore`'s `false` survived the 2026-07-31 re-ruling deliberately**, and the reason
|
||||
/// changed underneath it. It used to be "nothing in the app reads it", which stopped being
|
||||
/// true the day the file became the loose-file heal's noise gate (01-storage-format.md
|
||||
/// § Fractal layout ▸ Rules) — by the level-uniform timing principle (a name "joins the
|
||||
/// scheduled class the day the name becomes load-bearing", which is how `comments` graduated)
|
||||
/// a folder squatting it would now be a candidate. It stays undisplaced because **the app
|
||||
/// degrades to correctness without the file**: an unreadable noise definition means no
|
||||
/// exclusions, which is exactly the behaviour boards had before the ruling and exactly what an
|
||||
/// empty file — the ruling's own escape hatch — asks for. Nothing breaks while the name is
|
||||
/// held, so nothing is displaced; that is the "proactive only when the defect is load-bearing
|
||||
/// *now*" half of § Validation and healing. Neither 01 nor 06 asks for a displacement here,
|
||||
/// and inventing one would move a node of the user's to buy a courtesy file.
|
||||
public let displacesSquatters: Bool
|
||||
}
|
||||
|
||||
@@ -199,7 +219,7 @@ public enum IntegrityRules: Sendable {
|
||||
ClaimedName(name: trashFolderName, expected: .directory, displacesSquatters: true),
|
||||
ClaimedName(name: "CLAUDE.md", expected: .file, displacesSquatters: true),
|
||||
ClaimedName(name: "CLAUDE.user.md", expected: .file, displacesSquatters: false),
|
||||
ClaimedName(name: ".gitignore", expected: .file, displacesSquatters: false),
|
||||
ClaimedName(name: gitignoreFileName, expected: .file, displacesSquatters: false),
|
||||
]
|
||||
|
||||
/// **The card-level claimed names** — the same table one level down (01-storage-format.md § Fractal
|
||||
@@ -451,9 +471,15 @@ public enum IntegrityRules: Sendable {
|
||||
|
||||
// MARK: - Per-field validation (the rulebook)
|
||||
|
||||
/// `schema`, validated: present, well-formed, not newer than this app (01-storage-format.md
|
||||
/// § Malformed input). Required at every level.
|
||||
public static func validatedSchema(
|
||||
/// `schema` **at the board root**, validated: present, well-formed, not newer than this app
|
||||
/// (01-storage-format.md § Malformed input).
|
||||
///
|
||||
/// **The root keeps its requirement** (re-ruled 2026-07-31): the root's stamp is the board's
|
||||
/// identity claim — the this-really-is-a-board gate — so a root without one is the loud
|
||||
/// rejection it always was, and the decision surface's minted repair (`stamp schema: 1`) is
|
||||
/// what fixes it. Below the root the key is optional and defaults instead
|
||||
/// (`resolvedSchema(in:path:supportedSchema:)`).
|
||||
public static func validatedRootSchema(
|
||||
in document: FrontmatterDocument,
|
||||
path: String,
|
||||
supportedSchema: Int
|
||||
@@ -464,51 +490,111 @@ public enum IntegrityRules: Sendable {
|
||||
case let .malformed(raw):
|
||||
throw BoardLoadError(path: path, reason: .malformedSchema(raw: raw))
|
||||
case let .valid(value):
|
||||
guard value <= supportedSchema else {
|
||||
throw BoardLoadError(path: path, reason: .schemaNewerThanApp(found: value))
|
||||
}
|
||||
try checkNotNewerThanApp(value, path: path, supportedSchema: supportedSchema)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/// `order`, validated: present and well-formed. Required on lanes and cards, **never** on the
|
||||
/// board itself — which is the whole of the per-kind difference in the tables today.
|
||||
public static func validatedOrder(
|
||||
in document: FrontmatterDocument,
|
||||
path: String
|
||||
) throws(BoardLoadError) -> Double {
|
||||
switch document.order {
|
||||
case .missing:
|
||||
throw BoardLoadError(path: path, reason: .missingOrder)
|
||||
case let .malformed(raw):
|
||||
throw BoardLoadError(path: path, reason: .malformedOrder(raw: raw))
|
||||
case let .valid(value):
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an object of `kind` must carry `order` — the per-kind field table, as a rule rather
|
||||
/// than as two hand-written call sites in the loader's walk.
|
||||
/// `schema` **below the board root**, read: a missing (or explicitly null) key reads as **1**
|
||||
/// (01-storage-format.md § Frontmatter, re-ruled 2026-07-31 — "below the root a missing `schema`
|
||||
/// reads as 1 — coerce-tier, logged, bytes preserved — because the walk validating the file
|
||||
/// against schema 1 is exactly what makes the reading reliable").
|
||||
///
|
||||
/// **A comment carries none, and never gains one** (01-storage-format.md § Enhanced schema:
|
||||
/// "**No `title`, no `order`**" — "Ordering is chronology, not ranks", because a conversation's
|
||||
/// semantics *are* chronology and tracker-synced comments carry independent clocks where minted
|
||||
/// ranks would interleave arbitrarily).
|
||||
public static func requiresOrder(_ kind: ObjectKind) -> Bool {
|
||||
switch kind {
|
||||
case .board, .comment: false
|
||||
case .lane, .card: true
|
||||
/// Two things stay fail-fast here, and both are the same rule they always were:
|
||||
///
|
||||
/// - **A schema newer than the app**, at any level: there is no honest reading of a file written
|
||||
/// by a Lanework that knows keys this one does not, and defaulting would be guessing at content
|
||||
/// rather than recovering it.
|
||||
/// - **A malformed `schema`** — `schema: one`, `schema: [1]`. The ruling made the *absent* key
|
||||
/// optional, not the unreadable one: absence is what a hand-filed item legitimately looks like,
|
||||
/// while a value that is there and unreadable is a claim the app cannot check, and reading it
|
||||
/// as 1 would be inventing agreement.
|
||||
///
|
||||
/// - Returns: the reading, plus the coerce-tier record when one was made — the field, and the
|
||||
/// text as written (`nil` when the key was present and valid, which is every file this app
|
||||
/// ever wrote). The *path* is the loader's to attach (`CoercedFrontmatter`).
|
||||
public static func resolvedSchema(
|
||||
in document: FrontmatterDocument,
|
||||
path: String,
|
||||
supportedSchema: Int
|
||||
) throws(BoardLoadError) -> (schema: Int, coerced: CoercedField?) {
|
||||
switch document.schema {
|
||||
case .missing:
|
||||
return (defaultSchema, coercedRecord(for: FrontmatterKeys.schema, in: document, raw: nil))
|
||||
case let .malformed(raw):
|
||||
throw BoardLoadError(path: path, reason: .malformedSchema(raw: raw))
|
||||
case let .valid(value):
|
||||
try checkNotNewerThanApp(value, path: path, supportedSchema: supportedSchema)
|
||||
return (value, nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// The reading a `schema`-less file below the root gets — the schema this document *is* being
|
||||
/// validated against, which is the argument for the default rather than a coincidence.
|
||||
public static let defaultSchema = 1
|
||||
|
||||
private static func checkNotNewerThanApp(
|
||||
_ value: Int,
|
||||
path: String,
|
||||
supportedSchema: Int
|
||||
) throws(BoardLoadError) {
|
||||
guard value <= supportedSchema else {
|
||||
throw BoardLoadError(path: path, reason: .schemaNewerThanApp(found: value))
|
||||
}
|
||||
}
|
||||
|
||||
/// `order` **below the board root**, read: `nil` where the file carries no rank this app can
|
||||
/// use — which the caller reads as **append-at-end** (`Ranks.resolvedOrders(of:stored:name:)`).
|
||||
///
|
||||
/// **Four shapes, one reading** (01-storage-format.md § Ordering, re-ruled 2026-07-31 —
|
||||
/// "Missing or unusable `order` reads as append-at-end"): a missing key, an explicitly null one
|
||||
/// (`order:` bare, `order: null` — the null-as-missing rule), a non-numeric value, and a
|
||||
/// non-finite one (`.nan`, `.inf`, which has no place in the total order the tie-break and
|
||||
/// midpoint math assume). None of them is a load failure any more; all of them are coerce-tier —
|
||||
/// bytes preserved verbatim, the reading logged, the rank materializing on the file's first
|
||||
/// touch (`healOnTouch`).
|
||||
///
|
||||
/// It never throws, and has no `path`: with nothing left to refuse, there is nothing to name.
|
||||
///
|
||||
/// **The board root never asks.** A board carries no rank — it is not among siblings — so the
|
||||
/// key is meaningless there and this is simply not called on it, exactly as it never was.
|
||||
public static func resolvedOrder(
|
||||
in document: FrontmatterDocument
|
||||
) -> (order: Double?, coerced: CoercedField?) {
|
||||
switch document.order {
|
||||
case .valid(let value):
|
||||
return (value, nil)
|
||||
case .malformed(let raw):
|
||||
return (nil, coercedRecord(for: FrontmatterKeys.order, in: document, raw: raw))
|
||||
case .missing:
|
||||
return (nil, coercedRecord(for: FrontmatterKeys.order, in: document, raw: nil))
|
||||
}
|
||||
}
|
||||
|
||||
/// One coerce-tier record: the field and **the value exactly as written**, which for an absent
|
||||
/// key is the empty string — there is no text to record, and inventing a rendering of one would
|
||||
/// defeat the record's whole purpose (a shape observed in the wild, promotable later to a
|
||||
/// heuristic heal). An explicitly-null key answers with its own (empty) source span, so
|
||||
/// "started the key and never gave it a value" is preserved as exactly that.
|
||||
private static func coercedRecord(
|
||||
for key: String,
|
||||
in document: FrontmatterDocument,
|
||||
raw: String?
|
||||
) -> CoercedField {
|
||||
CoercedField(key: key, raw: raw ?? document.rawValue(for: key) ?? "")
|
||||
}
|
||||
|
||||
/// **Per-kind index validation** — the loader's own checks, in its own order, over bytes that
|
||||
/// need not be on disk yet (02-architecture.md ▸ Components: "the card validator generalized per
|
||||
/// kind — board, lane, card, the enhanced schema's comment when it lands").
|
||||
///
|
||||
/// Exactly the checks `BoardLoader.load` runs on an object of that kind, through its own
|
||||
/// functions: decode + parse, then `schema`, then `order` where the kind requires it. Nothing
|
||||
/// further is checked, because nothing else *is*: `title` is optional, unknown keys are the
|
||||
/// point of the outlet the card validator serves, and the body is free text.
|
||||
/// functions: decode + parse, then `schema` — at the root's strictness for a board, at the
|
||||
/// below-the-root reading for everything else. Nothing further is checked, because nothing else
|
||||
/// *is*: `title` is optional, `order` is optional below the root since 2026-07-31 (a rankless
|
||||
/// card applied through the raw-source outlet lands at its lane's bottom and gains a rank on its
|
||||
/// next touch, exactly as a hand-filed one does), unknown keys are the point of the outlet the
|
||||
/// card validator serves, and the body is free text.
|
||||
///
|
||||
/// It deliberately does **not** check `uneditableShape`: that refusal exists for surgical span
|
||||
/// edits, and the raw-source Apply this serves replaces the whole file — a flow-mapping
|
||||
@@ -520,9 +606,11 @@ public enum IntegrityRules: Sendable {
|
||||
supportedSchema: Int
|
||||
) throws(BoardLoadError) -> FrontmatterDocument {
|
||||
let document = try BoardLoader.parseDocument(data, path: path)
|
||||
_ = try validatedSchema(in: document, path: path, supportedSchema: supportedSchema)
|
||||
if requiresOrder(kind) {
|
||||
_ = try validatedOrder(in: document, path: path)
|
||||
switch kind {
|
||||
case .board:
|
||||
_ = try validatedRootSchema(in: document, path: path, supportedSchema: supportedSchema)
|
||||
case .lane, .card, .comment:
|
||||
_ = try resolvedSchema(in: document, path: path, supportedSchema: supportedSchema)
|
||||
}
|
||||
return document
|
||||
}
|
||||
@@ -551,6 +639,23 @@ public enum IntegrityRules: Sendable {
|
||||
/// add a key that is redundant with position everywhere else.
|
||||
case kindBackfilled(ObjectKind)
|
||||
|
||||
/// `order` was missing or unusable and has been stamped with the rank the board was
|
||||
/// **already rendering it at** — the append-at-end reading, materialized
|
||||
/// (01-storage-format.md § Ordering, re-ruled 2026-07-31: "The rank materializes on touch:
|
||||
/// the first Writer rewrite of the file stamps a real rank").
|
||||
///
|
||||
/// Nothing moves on screen when it lands, and that is the design rather than a happy
|
||||
/// accident: the stamped value is `Ranks.resolvedOrders`' own answer for this file, so the
|
||||
/// heal writes down what the reading already said. It is on-touch for the reason the whole
|
||||
/// class is — the defect is latent, the file is being rewritten anyway, and a sweep that
|
||||
/// rewrote a board to add ranks nobody asked for would be churn.
|
||||
///
|
||||
/// **Unlike `kind`, an unusable *present* value is replaced.** `order: banana` and
|
||||
/// `order: .nan` are read as order-less (the value-names-the-kind posture has no twin here:
|
||||
/// a rank must be a number for the midpoint math to mean anything), so the stamp is the same
|
||||
/// heal in both cases and the unreadable text does not survive the rewrite.
|
||||
case rankStamped(Double)
|
||||
|
||||
/// A key written twice collapsed to its winning (last) occurrence — the span editor's
|
||||
/// duplicate-key twin removal (`FrontmatterDocument.set`). Named here because it *is* an
|
||||
/// on-touch heal and was only ever documented as an editing detail: last-wins is the read
|
||||
@@ -569,28 +674,43 @@ public enum IntegrityRules: Sendable {
|
||||
/// `updateIndex` seam, which consults IntegrityRules for pending on-touch work on the file it is
|
||||
/// rewriting").
|
||||
///
|
||||
/// Only the `kind` backfill is applied *here*; the other two members of the class are applied by
|
||||
/// `FrontmatterDocument`'s own editor on every key it writes, and are named in `OnTouchHeal`
|
||||
/// rather than re-implemented. That is the honest shape of "the same class, named, no behavior
|
||||
/// change".
|
||||
/// Two of the four members are applied *here* — the `kind` backfill and the rank stamp; the
|
||||
/// other two are applied by `FrontmatterDocument`'s own editor on every key it writes, and are
|
||||
/// named in `OnTouchHeal` rather than re-implemented. That is the honest shape of "the same
|
||||
/// class, named, no behavior change".
|
||||
///
|
||||
/// - **Missing only.** A present `kind` is never rewritten, never corroborated, and never
|
||||
/// stripped — the value names the kind and consumers trust it outright. An explicit `kind:`
|
||||
/// with nothing after it reads as missing, like every other null (the null-as-missing rule),
|
||||
/// and so backfills.
|
||||
/// - **`kind`: missing only.** A present `kind` is never rewritten, never corroborated, and
|
||||
/// never stripped — the value names the kind and consumers trust it outright. An explicit
|
||||
/// `kind:` with nothing after it reads as missing, like every other null (the null-as-missing
|
||||
/// rule), and so backfills.
|
||||
/// - **`kind == nil` stamps nothing.** Position cannot always answer (`Placement.unknown`), and
|
||||
/// a guessed kind written to disk would be worse than an absent one: the trash discriminator
|
||||
/// trusts what it finds.
|
||||
/// - Parameter kind: `@autoclosure` so a caller whose answer costs a directory listing (the
|
||||
/// trash's shape fallback) pays for it only on a file that actually needs the backfill.
|
||||
/// - **`order`: missing *or unusable*.** The reading is stated over usability
|
||||
/// (`resolvedOrder(in:)`), so the stamp is too.
|
||||
/// - **`rank == nil` stamps nothing** — the board root (which has no siblings and no rank), a
|
||||
/// comment (which never gains one — "Ordering is chronology, not ranks"), and any folder whose
|
||||
/// position has no answer. The same posture as `kind`'s: a rank invented for something that is
|
||||
/// not a lane or a card would be a fact nobody asked for.
|
||||
/// - Parameters kind, rank: `@autoclosure` so a caller whose answer costs a directory listing
|
||||
/// (the trash's shape fallback; the rank's own sibling scan) pays for it only on a file that
|
||||
/// actually needs the heal — which, on a board this app wrote, is never.
|
||||
@discardableResult
|
||||
public static func healOnTouch(
|
||||
_ document: inout FrontmatterDocument,
|
||||
kind: @autoclosure () -> ObjectKind?
|
||||
kind: @autoclosure () -> ObjectKind?,
|
||||
rank: @autoclosure () -> Double? = nil
|
||||
) -> [OnTouchHeal] {
|
||||
guard document.kind.isMissing, let kind = kind() else { return [] }
|
||||
document.set(FrontmatterKeys.kind, to: .string(kind.rawValue))
|
||||
return [.kindBackfilled(kind)]
|
||||
var applied: [OnTouchHeal] = []
|
||||
if document.kind.isMissing, let kind = kind() {
|
||||
document.set(FrontmatterKeys.kind, to: .string(kind.rawValue))
|
||||
applied.append(.kindBackfilled(kind))
|
||||
}
|
||||
if document.order.value == nil, let rank = rank() {
|
||||
document.set(FrontmatterKeys.order, to: .double(rank))
|
||||
applied.append(.rankStamped(rank))
|
||||
}
|
||||
return applied
|
||||
}
|
||||
|
||||
// MARK: - The typed defect stream
|
||||
@@ -640,6 +760,12 @@ public enum IntegrityRules: Sendable {
|
||||
/// It is a class here because the engine treats it exactly like the others — same gates,
|
||||
/// same memo, same clear-on-success.
|
||||
///
|
||||
/// `missingGitignore` is the third, and the guide's twin in every respect: the board's noise
|
||||
/// definition is one board-root file's presence, read at the moment of healing
|
||||
/// (`BoardWriter.seedGitignoreIfAbsent`) rather than reported by a tree walk — "a board
|
||||
/// missing the file gains it by scheduled heal at open (the guide-refresh cadence)"
|
||||
/// (06-history-undo.md ▸ Repository hygiene, re-ruled 2026-07-31).
|
||||
///
|
||||
/// `commentTrashResidue` is the second such class, for the same reason one level down: the
|
||||
/// residue is whatever a crashed session left in one card's `comments/.trash/`, read at the
|
||||
/// moment the card window opens (01-storage-format.md § Enhanced schema — "purged when the
|
||||
@@ -652,6 +778,7 @@ public enum IntegrityRules: Sendable {
|
||||
case claimedNameSquatted
|
||||
case duplicateIdentity
|
||||
case staleAgentGuide
|
||||
case missingGitignore
|
||||
case commentTrashResidue
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user