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:
@@ -182,6 +182,30 @@ public enum IntegrityRules: Sendable {
|
||||
ClaimedName(name: ".gitignore", expected: .file, displacesSquatters: false),
|
||||
]
|
||||
|
||||
/// **The card-level claimed names** — the same table one level down (01-storage-format.md § Fractal
|
||||
/// layout ▸ Rules, extended 2026-07-29: "**The rule is level-uniform**: a card's reserved child
|
||||
/// names are claimed the same way — a regular file or symlink squatting `attachments` (a directory
|
||||
/// name) displaces by the same ladder (`attachments` → `attachments 2`), so imports, Finder drops,
|
||||
/// and the sidebar listing never fail one gesture at a time against a squatted name").
|
||||
///
|
||||
/// **`attachments` displaces; `comments` does not**, and the split is the *timing principle*
|
||||
/// rather than a hedge — 01 calls the reserved-but-unconsumed `comments` "the timing principle's own
|
||||
/// illustration": nothing reads that name until the tracker era, so a wrong-kind holder degrades no
|
||||
/// behavior while it stands and stays a **tolerated stray** today, joining the scheduled class the
|
||||
/// day the name becomes load-bearing. `attachments`, by contrast, is load-bearing now: while a file
|
||||
/// wears the name, every import into that card, every Finder drop on it, and the card window's
|
||||
/// listing are broken — which is exactly the "proactive when the defect is load-bearing now"
|
||||
/// condition (§ Validation and healing).
|
||||
///
|
||||
/// `index.md` is deliberately not here. It is not a *reserved child* the app protects from
|
||||
/// squatters — it is the card's content, and a directory named `index.md` makes the folder an
|
||||
/// index-less stray the loader already skips with a warning (§ Fractal layout ▸ Rules). Displacing it
|
||||
/// would mean the app deciding a folder's content is a squatter.
|
||||
public static let claimedCardChildNames: [ClaimedName] = [
|
||||
ClaimedName(name: attachmentsFolderName, expected: .directory, displacesSquatters: true),
|
||||
ClaimedName(name: "comments", expected: .directory, displacesSquatters: false),
|
||||
]
|
||||
|
||||
/// The claimed names as the lane walk needs them: lowercased, for a `contains` against a
|
||||
/// directory entry. Compared lowercased for `reservedCardChildNames`' reason.
|
||||
public static let claimedRootNameSet: Set<String> = Set(claimedRootNames.map { $0.name.lowercased() })
|
||||
@@ -222,6 +246,38 @@ public enum IntegrityRules: Sendable {
|
||||
return ClaimedNameSquatter(name: claimed.name, found: found, expected: claimed.expected)
|
||||
}
|
||||
|
||||
/// The claimed-name defects inside one **card** folder — the level-uniform half of the same ruling
|
||||
/// (`claimedCardChildNames`, extended 2026-07-29).
|
||||
///
|
||||
/// Only names whose `displacesSquatters` is `true` can produce one, which today means `attachments`
|
||||
/// and only `attachments`: a `comments` held by the wrong kind of node is a tolerated stray until
|
||||
/// the feature consumes the name.
|
||||
///
|
||||
/// **A plural answer, unlike the board root's**, because the reason the root's is singular does not
|
||||
/// apply here: there, the second displacing name (`CLAUDE.md`) is the agent guide's own to heal, so
|
||||
/// answering it twice would be two mechanisms racing one node. Nothing else owns a card's children,
|
||||
/// so this returns every offender it finds and the table stays the only thing to edit when
|
||||
/// `comments` graduates.
|
||||
///
|
||||
/// - Parameter path: the card folder's path **relative to the board root**, carried into the defect
|
||||
/// so the write lands wherever the board lives at heal time (`LooseCardFiles`' convention).
|
||||
public static func squattedClaimedNames(inCardAt cardFolder: URL, path: String) -> [ClaimedNameSquatter] {
|
||||
claimedCardChildNames.compactMap { claimed in
|
||||
guard claimed.displacesSquatters,
|
||||
let found = node(at: cardFolder.appendingPathComponent(claimed.name)),
|
||||
found != claimed.expected
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return ClaimedNameSquatter(
|
||||
name: claimed.name,
|
||||
found: found,
|
||||
expected: claimed.expected,
|
||||
location: .card(path: path)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Object kinds
|
||||
|
||||
/// The kinds the schema knows (01-storage-format.md § Frontmatter ▸ Common to all levels, the
|
||||
@@ -454,6 +510,20 @@ public enum IntegrityRules: Sendable {
|
||||
/// and reminted by the scheduled heal (re-ruled 2026-07-29 — the silent remint).
|
||||
case duplicateIdentity(DuplicateIdentity)
|
||||
|
||||
/// **A coerce-tier fallback**: one file's lenient fields that had no sensible reading and
|
||||
/// rendered as their defaults (ruled 2026-07-29 — "A no-sensible-reading fallback logs: field,
|
||||
/// path, and raw text, carried as coerce-tier entries in the integrity service's Defect
|
||||
/// stream").
|
||||
///
|
||||
/// **The one case in this enum that is not work**, which is the ruling rather than a
|
||||
/// contradiction: the design puts coerce-tier fallbacks in *this* stream on purpose, because
|
||||
/// this is where "an observed-in-the-wild shape can later be promoted to a heuristic heal or a
|
||||
/// notice" — and the promotion would happen right here, by giving the case a heal class. Until
|
||||
/// then it has none (`healClass` answers `nil`), raises no banner, and changes no behavior: the
|
||||
/// value already rendered as its default and the bytes on disk are untouched. It is
|
||||
/// observability, carried in the vocabulary that would act on it if the app ever decided to.
|
||||
case coercedFrontmatter(CoercedFrontmatter)
|
||||
|
||||
/// The scheduled-heal classes, which are also the engine's memo keys and its
|
||||
/// banner-posture rows (`HealScheduler`).
|
||||
///
|
||||
@@ -470,12 +540,18 @@ public enum IntegrityRules: Sendable {
|
||||
case staleAgentGuide
|
||||
}
|
||||
|
||||
public var healClass: Class {
|
||||
/// The scheduled-heal class this defect belongs to, or **`nil` where there is no heal** — the
|
||||
/// coerce tier (`coercedFrontmatter`), which is carried for observability and acted on by
|
||||
/// nothing. Optional rather than a synthetic class, because a class *is* a memo key and a
|
||||
/// banner-posture row in the engine (`HealScheduler`): inventing one for work that does not
|
||||
/// exist would arm a memo against a repair nobody wrote.
|
||||
public var healClass: Class? {
|
||||
switch self {
|
||||
case .looseCardFiles: .looseCardFiles
|
||||
case .legacyTombstone: .legacyTombstone
|
||||
case .claimedNameSquatted: .claimedNameSquatted
|
||||
case .duplicateIdentity: .duplicateIdentity
|
||||
case .coercedFrontmatter: nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,13 +571,21 @@ public enum IntegrityRules: Sendable {
|
||||
case let .claimedNameSquatted(work):
|
||||
// The node *kind* is part of the picture: a squatter replaced by a different kind
|
||||
// of squatter is a new defect, and a heal that failed on one has no claim to have
|
||||
// failed on the other.
|
||||
["claimed:\(work.name):\(work.found.rawValue)"]
|
||||
// failed on the other. The location leads, so two cards squatting `attachments` are two
|
||||
// pieces of work — and a board-root squatter signs exactly as it always did.
|
||||
["claimed:\(work.location.signatureComponent)\(work.name):\(work.found.rawValue)"]
|
||||
case let .duplicateIdentity(work):
|
||||
// The *identity* is part of the picture beside the path: the same folder losing a
|
||||
// different collision (its winner reminted, a third copy landing) is new work, and a
|
||||
// heal that failed on one has no claim to have failed on the other.
|
||||
["duplicate:\(work.path):\(work.identity)"]
|
||||
case let .coercedFrontmatter(work):
|
||||
// One signature per field, `looseCardFiles`' shape: the unit of the observation is a
|
||||
// field, and a file whose `width` healed while its `icon` did not is a changed picture.
|
||||
// Nothing memoizes these today — there is no heal to guard — but a signature is what a
|
||||
// defect *is* in this vocabulary, and omitting it would make this case the one members
|
||||
// of the stream cannot be compared by.
|
||||
work.fields.map { "coerce:\(work.path):\($0.key)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -805,18 +889,59 @@ public struct LegacyTombstone: Sendable, Equatable {
|
||||
/// **An invalid artifact, not a resident.** The heal moves it aside via the Finder-style rename
|
||||
/// ladder (`.trash` → `.trash 2`), preserved verbatim and never destroyed, with a warning-tone
|
||||
/// notice naming old and new — the invariant that survives is displacement-never-destruction.
|
||||
/// **The rule is level-uniform** (extended 2026-07-29): a card's `attachments` is claimed exactly as the
|
||||
/// board root's `.trash` is, and displaces by the same ladder — which is why `location` exists rather
|
||||
/// than a second defect type. The heals compose: the displaced file, now an ordinary loose file, rides
|
||||
/// the next loose-file relocation into the real `attachments/`.
|
||||
public struct ClaimedNameSquatter: Sendable, Equatable {
|
||||
/// The claimed name, exactly as the app spells it (`.trash`, `CLAUDE.md`).
|
||||
|
||||
/// **Which claimed name this is** — the board root's, or one card's reserved child.
|
||||
///
|
||||
/// A path rather than a URL, relative to the board root, so the heal joins it onto the store's
|
||||
/// *current* root and a board renamed mid-session heals at its new location (`LooseCardFiles`' and
|
||||
/// `IdentityOccurrence.path`'s convention).
|
||||
public enum Location: Sendable, Equatable {
|
||||
case boardRoot
|
||||
case card(path: String)
|
||||
|
||||
/// The folder the claimed name lives in, under `root`.
|
||||
public func folder(under root: URL) -> URL {
|
||||
switch self {
|
||||
case .boardRoot: root
|
||||
case let .card(path): root.appendingPathComponent(path, isDirectory: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// The location as a signature component — `""` for the board root, so the existing root-level
|
||||
/// signature spelling is unchanged and only a card-level defect adds a path segment.
|
||||
var signatureComponent: String {
|
||||
switch self {
|
||||
case .boardRoot: ""
|
||||
case let .card(path): path + "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The claimed name, exactly as the app spells it (`.trash`, `CLAUDE.md`, `attachments`).
|
||||
public let name: String
|
||||
/// What is actually sitting there — never followed if it is a symlink.
|
||||
public let found: IntegrityRules.NodeKind
|
||||
/// What the app needs the name to be.
|
||||
public let expected: IntegrityRules.NodeKind
|
||||
/// Where the name lives. Defaults to the board root, which is where every squatter was before the
|
||||
/// rule went level-uniform — so the root-level call sites and their tests read exactly as they did.
|
||||
public let location: Location
|
||||
|
||||
public init(name: String, found: IntegrityRules.NodeKind, expected: IntegrityRules.NodeKind) {
|
||||
public init(
|
||||
name: String,
|
||||
found: IntegrityRules.NodeKind,
|
||||
expected: IntegrityRules.NodeKind,
|
||||
location: Location = .boardRoot
|
||||
) {
|
||||
self.name = name
|
||||
self.found = found
|
||||
self.expected = expected
|
||||
self.location = location
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,6 +980,33 @@ public struct DuplicateIdentity: Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// One file's **coerce-tier fallbacks**: the lenient fields whose value had no sensible reading, so the
|
||||
/// field rendered as its default (01-storage-format.md § Frontmatter, ruled 2026-07-29: "A
|
||||
/// no-sensible-reading fallback logs: field, path, and raw text, carried as coerce-tier entries in the
|
||||
/// integrity service's Defect stream — the one place where an observed-in-the-wild shape can later be
|
||||
/// promoted to a heuristic heal or a notice; no banner, no behavior change").
|
||||
///
|
||||
/// **Per file, not per field** — `LooseCardFiles`' shape and for its reason: the walk meets a document
|
||||
/// once and reads all of its fields there, so one record per `index.md` is what the loader naturally
|
||||
/// has, and a caller that wants per-field granularity has `fields` (and the per-field `signatures`).
|
||||
///
|
||||
/// The path is root-relative, as every load-side path in this app is (`BoardLoadError.path`,
|
||||
/// `IdentityOccurrence.path`) — it names the `index.md`, because that is the file whose bytes were
|
||||
/// read and the thing a developer would open.
|
||||
public struct CoercedFrontmatter: Sendable, Equatable {
|
||||
/// The `index.md`'s path relative to the board root — `"index.md"`, `"<lane>/index.md"`,
|
||||
/// `"<lane>/<card>/index.md"`, `".trash/<card>/index.md"`.
|
||||
public let path: String
|
||||
/// The fields that fell back, in schema order. Never empty — a document that read cleanly
|
||||
/// contributes no defect at all.
|
||||
public let fields: [CoercedField]
|
||||
|
||||
public init(path: String, fields: [CoercedField]) {
|
||||
self.path = path
|
||||
self.fields = fields
|
||||
}
|
||||
}
|
||||
|
||||
/// A folder whose name is a **case-spelled twin** of another occurrence of the same identity — one
|
||||
/// item typed two ways, not two items (01-storage-format.md § Fractal layout ▸ Rules: "the canonical
|
||||
/// all-lowercase spelling wins where present, else the lexicographically first spelling; the loser
|
||||
|
||||
Reference in New Issue
Block a user