Realign read-side rules — width range coercion, finite order, symlink pins

The design corpus ratified that ranges are part of a sensible reading:
an exact-integer width below 1 now coerces to 1 read-side (bytes
untouched) instead of reading as malformed — the width division must
never see a zero or negative unit — while a non-finite order (.nan,
.inf) is now the same loud malformed-order rejection as a non-numeric
one, guarded at the single point where the double arrives so loader
and Writer inherit it together. The symlink-never-traversed rule
turned out to be already enforced (the loader has filtered symlinks
ahead of the directory check since the first commit); it and the
copy-preserves-the-link-verbatim behavior are now pinned by tests,
alongside the two hostile shapes the corpus names (width: 0,
order: .nan). Five new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-27 14:21:03 -04:00
parent b4c90838b4
commit bea6d02d1d
6 changed files with 155 additions and 27 deletions
+20 -9
View File
@@ -46,11 +46,16 @@ extension FrontmatterDocument {
read(FrontmatterKeys.schema) { value, _ in if case let .int(value) = value { value } else { nil } }
}
/// A non-finite reading (`.nan`, `.inf`) has no place in the total order the tie-break and
/// midpoint math assume (01-storage-format.md § Frontmatter, settled) it is the same loud
/// malformed-input rejection as a non-numeric value, not a `.valid(Double.nan)` silently
/// poisoning every comparison downstream. An `Int` reading is always finite, so only the
/// `.double` case needs the check.
public var order: FieldValue<Double> {
read(FrontmatterKeys.order) { value, _ in
switch value {
case let .int(value): Double(value)
case let .double(value): value
case let .double(value): value.isFinite ? value : nil
default: nil
}
}
@@ -67,13 +72,16 @@ extension FrontmatterDocument {
public var icon: FieldValue<String> { read(FrontmatterKeys.icon, Self.string) }
public var iconColor: FieldValue<String> { read(FrontmatterKeys.iconColor, Self.string) }
/// Width multiplier. An int stays; a string or double with an exact integer reading 1
/// coerces (`"2"`, `2.0` `2`). Everything else zero, negative, fractional, non-numeric,
/// bool, a sequence/mapping is malformed and renders as the default 1.
/// Width multiplier. An exact-integer reading from an int, a double, or a numeric string
/// always coerces: at or above 1 to itself (`"2"`, `2.0` `2`), below 1 to 1 (**ranges are
/// part of the sensible reading**, 01-storage-format.md § Frontmatter, settled the table's
/// " 1" is a validity bound on the coerced reading, not a gate on which readings are
/// sensible). Everything else fractional, non-numeric, bool, a sequence/mapping has no
/// integer reading at all and is malformed, rendering as the default 1.
public var width: FieldValue<Int> {
read(FrontmatterKeys.width) { value, _ in
switch value {
case let .int(value): value >= 1 ? value : nil
case let .int(value): value >= 1 ? value : 1
case let .double(value): Self.exactIntWidth(value)
case let .string(text): Double(text).flatMap(Self.exactIntWidth)
default: nil
@@ -109,11 +117,14 @@ extension FrontmatterDocument {
}
}
/// An integer-valued double or numeric string 1 coerces; anything else (fractional,
/// non-numeric, out of `Int` range) has no sensible width reading.
/// An integer-valued double or numeric string coerces below 1 to 1, at or above 1 to the
/// value itself; a fractional reading, a non-numeric one, or one outside `Int` range on the
/// high end has no sensible width reading at all. The high-end guard is what makes `Int(value)`
/// safe below; there is no matching low-end guard because anything below 1 short-circuits to
/// the literal `1` without ever converting the (possibly enormous negative) double to `Int`.
private static func exactIntWidth(_ value: Double) -> Int? {
guard value.truncatingRemainder(dividingBy: 1) == 0, value >= 1, value <= Double(Int.max) else { return nil }
return Int(value)
guard value.truncatingRemainder(dividingBy: 1) == 0, value <= Double(Int.max) else { return nil }
return value >= 1 ? Int(value) : 1
}
/// A quoted timestamp reads the same as an unquoted one same YAML 1.1 timestamp grammar,
+9 -7
View File
@@ -50,13 +50,15 @@ enum LaneLayoutMath {
/// The whole units a lane spans on screen: its `width` when that read as a valid integer, 1
/// otherwise.
///
/// `Lane.width` is a **lenient** field (01-storage-format.md § Frontmatter): a missing key, a
/// non-numeric value, a fraction, a zero or a negative all arrive here as `.missing` or
/// `.malformed` and render as one unit the bytes on disk are left exactly as the author wrote
/// them until the user actually changes the width, at which point the Writer replaces them with
/// an integer (`BoardStore.setLaneWidth`). The `max(1,)` is belt over braces: the read side
/// already refuses anything below 1, and this function is the single place the rest of the UI
/// asks "how many units does this lane span".
/// `Lane.width` is a **lenient** field (01-storage-format.md § Frontmatter): a missing key or
/// a non-numeric/fractional value arrives here as `.missing` or `.malformed` and renders as
/// one unit, while an exact-integer reading below 1 (zero, negative) is no longer malformed at
/// all it coerces to 1 at the read side (**ranges are part of the sensible reading**,
/// settled). Either way the bytes on disk are left exactly as the author wrote them until the
/// user actually changes the width, at which point the Writer replaces them with an integer
/// (`BoardStore.setLaneWidth`). The `max(1,)` is belt over braces: the read side already never
/// produces anything below 1, and this function is the single place the rest of the UI asks
/// "how many units does this lane span".
static func displayUnits(of lane: Lane) -> Int {
max(1, lane.width.value ?? 1)
}