Build the frontmatter engine with byte-perfect round-trip

Yams validates and reads; writes are surgical line-span edits over
retained raw text, so untouched bytes — unknown keys, comments, odd
formatting, bodies — round-trip identically by construction. Strict
schema/order readers distinguish valid/missing/malformed for the
loader's fail-fast; lenient fields preserve malformed values verbatim;
modified-by is schema-owned. Adds the Yams package and the fixture
folder-reference wiring. 44 tests (105 cases).

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
2026-07-26 15:26:12 -04:00
parent 954c4b351d
commit cb6f6100a7
8 changed files with 1162 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
import Foundation
import Yams
/// A `Sendable` snapshot of a parsed YAML value. Mappings keep document order; this is a
/// read-only view the engine never serializes through it (see `FrontmatterDocument`).
public enum YAMLValue: Sendable, Equatable {
case null
case bool(Bool)
case int(Int)
case double(Double)
case string(String)
case date(Date)
case sequence([YAMLValue])
case mapping([Pair])
public struct Pair: Sendable, Equatable {
public let key: YAMLValue
public let value: YAMLValue
public init(key: YAMLValue, value: YAMLValue) {
self.key = key
self.value = value
}
}
}
extension YAMLValue {
init(_ node: Node, resolver: Resolver = .default) {
switch node {
case let .scalar(scalar):
self = YAMLValue(scalar: scalar, tag: resolver.resolveTag(of: node))
case let .sequence(sequence):
self = .sequence(sequence.map { YAMLValue($0, resolver: resolver) })
case let .mapping(mapping):
self = .mapping(mapping.map {
Pair(key: YAMLValue($0.key, resolver: resolver), value: YAMLValue($0.value, resolver: resolver))
})
case .alias:
self = .null
}
}
private init(scalar: Node.Scalar, tag: Tag.Name) {
switch tag {
case .null:
self = .null
case .bool:
self = Bool.construct(from: scalar).map(YAMLValue.bool) ?? .string(scalar.string)
case .int:
self = Int.construct(from: scalar).map(YAMLValue.int)
?? Double.construct(from: scalar).map(YAMLValue.double)
?? .string(scalar.string)
case .float:
self = Double.construct(from: scalar).map(YAMLValue.double) ?? .string(scalar.string)
case .timestamp:
self = Date.construct(from: scalar).map(YAMLValue.date) ?? .string(scalar.string)
default:
self = .string(scalar.string)
}
}
}
extension YAMLValue: CustomStringConvertible {
/// Diagnostic rendering only never used to write files.
public var description: String {
switch self {
case .null: "null"
case let .bool(value): value ? "true" : "false"
case let .int(value): String(value)
case let .double(value): String(value)
case let .string(value): value
case let .date(value): value.formatted(.iso8601)
case let .sequence(values): "[" + values.map(\.description).joined(separator: ", ") + "]"
case let .mapping(pairs):
"{" + pairs.map { "\($0.key.description): \($0.value.description)" }.joined(separator: ", ") + "}"
}
}
}