Build the Raw Source outlet
The escape hatch: View > Raw Source (opt-cmd-E) unmounts the whole content area for the literal on-disk index.md in a plain monospaced editor with Cancel/Apply. Raw source is window-level state, not a third body mode — entry rides setMode(.preview), which flushes the Edit session by construction, then reads the file fresh; exit reveals Preview, and an empty body after Apply does not reopen Edit (openIfNeeded already ran). Apply validates the proposed bytes through the loader's own card checks — parseDocument's strict UTF-8/BOM rejection, schema, order — deliberately skipping the uneditable-shape refusal, since a flow-mapping card is exactly what the hatch repairs; invalid bytes alert in place with the loader's own error and no bracket opens. The write is byte-for-byte with no modified stamp and no modified-by clear, per 01's explicit carve-out — the verbatim contract outranks stamping — and identical bytes write nothing. Escape cancels, cmd-Return applies, toggle-off applies too, and cmd-E disables while raw is active via a testable predicate. Tombstoned targets refuse as vanished: a foreign delete is never reverted by a stale buffer. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -400,21 +400,30 @@ public enum BoardLoader: Sendable {
|
||||
/// explicit that a BOM'd file is rejected at load (it fails the frontmatter delimiter);
|
||||
/// decoding byte-faithfully is what makes that stated rejection actually happen.
|
||||
private static func readDocument(at url: URL, path: String) throws(BoardLoadError) -> FrontmatterDocument {
|
||||
let text: String
|
||||
let data: Data
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
guard let decoded = String(validating: data, as: UTF8.self) else {
|
||||
throw BoardLoadError(path: path, reason: .unparseableYAML(message: "file is not UTF-8", line: nil))
|
||||
}
|
||||
text = decoded
|
||||
} catch let error as BoardLoadError {
|
||||
throw error
|
||||
data = try Data(contentsOf: url)
|
||||
} catch {
|
||||
throw BoardLoadError(
|
||||
path: path,
|
||||
reason: .unparseableYAML(message: "could not read file: \(error.localizedDescription)", line: nil)
|
||||
)
|
||||
}
|
||||
return try parseDocument(data, path: path)
|
||||
}
|
||||
|
||||
/// The decode-and-parse half of `readDocument(at:path:)`, over bytes rather than a URL.
|
||||
///
|
||||
/// Split out for the raw-source outlet, which validates bytes that are **not on disk yet**
|
||||
/// (`validateCardIndex`) — and split rather than copied on purpose: "Apply validates through the
|
||||
/// same fail-fast parse the loader uses" (05-card-window.md ▸ Raw source outlet) is only true if
|
||||
/// it is literally the same function. The strict UTF-8 decode is half of what that buys — a BOM'd
|
||||
/// or non-UTF-8 proposal is rejected here by the same two lines that reject one on disk
|
||||
/// (01-storage-format.md § Fractal layout ▸ Rules).
|
||||
static func parseDocument(_ data: Data, path: String) throws(BoardLoadError) -> FrontmatterDocument {
|
||||
guard let text = String(validating: data, as: UTF8.self) else {
|
||||
throw BoardLoadError(path: path, reason: .unparseableYAML(message: "file is not UTF-8", line: nil))
|
||||
}
|
||||
|
||||
do {
|
||||
return try FrontmatterDocument.parse(text)
|
||||
@@ -424,6 +433,32 @@ public enum BoardLoader: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `data` would load as a **card's** `index.md` — the raw-source Apply's gate
|
||||
/// (05-card-window.md ▸ Raw source outlet: "Apply validates through the same fail-fast parse the
|
||||
/// loader uses (detailed alert on error, stays in source mode) before writing byte-for-byte").
|
||||
///
|
||||
/// **Exactly the three checks `load(boardRoot:)` runs on a card**, in its order and through its
|
||||
/// own functions: decode + parse (`parseDocument`), then `schema` (present, well-formed, not
|
||||
/// newer than this app) and `order` (present, well-formed) — the two fields a card must carry.
|
||||
/// Nothing card-shaped is checked beyond that, because nothing else *is*: `title` is optional,
|
||||
/// unknown keys are the whole point of the outlet, and the body is free text.
|
||||
///
|
||||
/// It deliberately does **not** check `uneditableShape`: that refusal exists for surgical
|
||||
/// span edits (`BoardWriter.updateIndex`), and raw source replaces the whole file — a flow-mapping
|
||||
/// frontmatter is precisely one of the things the escape hatch exists to let a user rewrite.
|
||||
///
|
||||
/// The error is the loader's own, undiluted, so the alert can show the taxonomy's display text
|
||||
/// (line numbers included) rather than a re-worded copy.
|
||||
///
|
||||
/// - Parameter path: what the error names — `indexFileName` from every call site today, which is
|
||||
/// what the card window's alert is about.
|
||||
public static func validateCardIndex(_ data: Data, path: String) throws(BoardLoadError) -> FrontmatterDocument {
|
||||
let document = try parseDocument(data, path: path)
|
||||
_ = try validatedSchema(in: document, path: path)
|
||||
_ = try validatedOrder(in: document, path: path)
|
||||
return document
|
||||
}
|
||||
|
||||
private static func validatedSchema(in document: FrontmatterDocument, path: String) throws(BoardLoadError) -> Int {
|
||||
switch document.schema {
|
||||
case .missing:
|
||||
|
||||
Reference in New Issue
Block a user