Gate level detection on UUID folder-name shape

Only lowercase-hex 8-4-4-4-12 folder names are lane/card candidates;
anything else is a stray — skipped with a distinct warning, never
descended, never able to fail-fast a load. UUID-shaped folders keep the
prior contract (missing index skips, malformed frontmatter fail-fasts).
Design resolution from the Redesign board. +5 tests.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
2026-07-26 16:03:39 -04:00
parent 30af21a66e
commit 31f7691062
3 changed files with 263 additions and 67 deletions
+55 -7
View File
@@ -8,13 +8,21 @@ import os
/// partial result.
///
/// Level is position: root `index.md` board, depth-1 folders lanes, depth-2 folders
/// cards. Any non-reserved directory containing `index.md` at those depths is a level
/// regardless of its name no UUID-shape filtering, no name-based gating.
/// cards. **Name shape gates level detection** (01-storage-format.md § Fractal layout
/// Rules): only a folder whose name has UUIDv4's shape lowercase hex, `8-4-4-4-12` is a
/// lane/card *candidate* at those depths; see `isUUIDShaped` below for exactly what's checked.
/// Anything else even a directory holding a perfectly valid `index.md` is a stray: skipped
/// with a `.nonUUIDFolderIgnored` warning, preserved verbatim on disk, and never descended
/// into. A hand-made `notes/` folder (or a broken `index.md` inside one) can never brick a
/// load; only a UUID-shaped candidate that is itself missing `index.md` still gets the older
/// `.missingIndex` warning, and only a UUID-shaped candidate's `index.md` can fail-fast.
///
/// Reserved child names (`attachments/`, `comments/`) only matter as children *of a card*
/// (01-storage-format.md § Fractal layout Rules); since cards are leaves here this loader
/// never scans a card folder's contents beyond checking for `index.md` that reservation is
/// satisfied by construction and needs no explicit filtering.
/// satisfied by construction and needs no explicit filtering. Doubly so under the shape rule:
/// were a card folder ever scanned, `attachments` and `comments` are non-UUID-shaped and would
/// read as strays, not levels so they never need special-casing against the stray warning.
///
/// Symlinks: a lane/card candidate that is itself a symlink is treated as a stray and never
/// followed, whether it points to a file or a directory this loader does not resolve
@@ -62,6 +70,10 @@ public enum BoardLoader: Sendable {
var lanes: [Lane] = []
for laneURL in try directoryCandidates(in: boardRoot) {
let laneName = laneURL.lastPathComponent
guard isUUIDShaped(laneName) else {
warn(.nonUUIDFolderIgnored(path: laneName))
continue
}
guard hasIndex(laneURL) else {
warn(.missingIndex(path: laneName))
continue
@@ -76,6 +88,10 @@ public enum BoardLoader: Sendable {
for cardURL in try directoryCandidates(in: laneURL) {
let cardName = cardURL.lastPathComponent
let cardRelPath = laneName + "/" + cardName
guard isUUIDShaped(cardName) else {
warn(.nonUUIDFolderIgnored(path: cardRelPath))
continue
}
guard hasIndex(cardURL) else {
warn(.missingIndex(path: cardRelPath))
continue
@@ -155,10 +171,31 @@ public enum BoardLoader: Sendable {
FileManager.default.fileExists(atPath: folder.appendingPathComponent(indexFileName).path)
}
/// The lowercase hex characters `isUUIDShaped` accepts in each `-`-delimited group.
private static let lowercaseHexDigits = Set("0123456789abcdef")
/// Whether `name` has UUIDv4's shape lowercase hex, `8-4-4-4-12` gating lane/card level
/// detection (01-storage-format.md § Fractal layout Rules, "Name shape gates level
/// detection"). Deliberately permissive about *which* nibbles matter: the version (13th hex
/// digit) and variant (17th hex digit) are **not** validated, so any lowercase-hex string in
/// this shape reads as a candidate whether or not it was actually minted by
/// `UUID().uuidString.lowercased()`. That reading is intentional, not an oversight: the
/// loader's job is recognizing the folder-naming *convention*, not re-deriving RFC 4122
/// conformance every load. Case-sensitive an uppercase or mixed-case UUID string is a
/// stray, matching `ItemID`'s byte-perfect, never-normalized storage of the folder name
/// (`BoardModel.swift`).
private static func isUUIDShaped(_ name: String) -> Bool {
let groups = name.split(separator: "-", omittingEmptySubsequences: false)
guard groups.map(\.count) == [8, 4, 4, 4, 12] else { return false }
return groups.allSatisfy { $0.allSatisfy(lowercaseHexDigits.contains) }
}
/// Direct subdirectories of `folder`, in deterministic (folder-name) order, excluding
/// hidden entries (`.DS_Store`, `.git`, ) and symlinks the loader's uniform stray
/// tolerance (01-storage-format.md § Fractal layout Rules). Stray *files* are excluded
/// here too: only directories are level candidates.
/// here too: only directories are level candidates at all, and the caller further narrows
/// those to actual lane/card candidates by name shape (`isUUIDShaped`) before doing
/// anything else with them.
///
/// An unreadable non-root folder (permission changed mid-walk, races) degrades to "no
/// candidates" rather than failing the whole load fail-fast is reserved for the board
@@ -243,11 +280,20 @@ public struct LoadResult: Sendable {
/// A tolerated anomaly the loader kept going past. Never blocks a load see `BoardLoadError`
/// for what does.
public enum LoadWarning: Sendable, Equatable, CustomStringConvertible {
/// A folder below the board root has no `index.md` skipped, not fail-fast (an
/// interrupted two-step create must not brick the board). `path` is relative to the board
/// root.
/// A **UUID-shaped** folder below the board root has no `index.md` skipped, not
/// fail-fast (an interrupted two-step create must not brick the board). `path` is relative
/// to the board root. Only reachable for a folder that passed `isUUIDShaped`; a
/// non-UUID-shaped folder missing `index.md` gets `.nonUUIDFolderIgnored` instead, never
/// this case.
case missingIndex(path: String)
/// A lane/card-depth folder whose name doesn't have UUIDv4's shape (`isUUIDShaped`)
/// skipped, not fail-fast, regardless of whether it holds a valid `index.md`, a broken one,
/// or none at all (01-storage-format.md § Fractal layout Rules, "Name shape gates level
/// detection"). Preserved verbatim on disk, never descended into. `path` is relative to the
/// board root.
case nonUUIDFolderIgnored(path: String)
/// A board-level `deleted:` key is legal per the frontmatter table but meaningless
/// (01-storage-format.md § Deletion) ignored, never tombstones the board.
case boardLevelDeletedIgnored
@@ -256,6 +302,8 @@ public enum LoadWarning: Sendable, Equatable, CustomStringConvertible {
switch self {
case let .missingIndex(path):
"\(path): folder has no index.md, skipped"
case let .nonUUIDFolderIgnored(path):
"\(path): folder name is not UUID-shaped, ignored as a stray"
case .boardLevelDeletedIgnored:
"index.md: board-level 'deleted' key is meaningless, ignored"
}