compliance: derive the A-G band instead of accepting one #64

Merged
dcharlot merged 1 commit from compliance/banding-function into main 2026-08-29 03:09:24 -04:00

2026-08-27

David Charlot
compliance: derive the A-G band instead of accepting one
All checks were successful
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 59s
`EnergyLabel` is an A–G enum with an ordering and a description, and nothing in
this workspace ever *derived* one — every use supplies a letter the caller had
already decided (`build_sample("svc-1", 0.35, EnergyLabel::A)`). The label was a
container for someone else's judgement, which is the one shape a rating must not
have. The only threshold rule that did exist,
`EuComplianceReport::is_compliant`, is a hardcoded `pue < 1.5 && renewable_pct
>= 50.0`: two magic floats with no provenance, no version, and no way to band
anything.

`BandingScale::rate` is the missing function. What matters is less the
arithmetic than what it refuses to let a caller do.

**A letter is not a rating.** Two facilities both rated "B" under different
threshold sets are not comparable, and nothing about the letter says so. A
`Rating` therefore cannot be constructed without its scale: it carries the
`scale_id`, the metric, the unit, and the measured value, so a reader
re-derives the letter rather than trusting it. `comparable_with` is an explicit
check, and `better_than` returns `None` for incomparable ratings rather than
`Ordering::Equal` — refusing to rank is a different answer from ranking equal,
and collapsing them is exactly how a bare enum invites the mistake.

**A revision is a different scale.** Because the `scale_id` travels inside every
rating, re-tuning thresholds cannot silently re-band history — the test pins a
value that is C under v1 and D under v2, and shows the two ratings do not
compare.

**Scales are data, not match arms.** A new scale is a value, not a recompile.

**Integers, in a declared unit.** Thresholds are `u64` in a unit the scale names
(milli-PUE rather than `1.5`), matching the receipt family's discipline: a
float boundary makes the band depend on how the value was parsed, and band edges
are precisely where that bites.

**Boundaries earn the better band**, stated once and tested at every edge, so
"on the line" is not left to the reader.

**A non-monotonic scale cannot rate at all.** Overlapping boundaries would
assign different letters to one value depending on evaluation order, so
`validate` rejects them and `rate` refuses rather than emitting a band from a
scale that cannot rank. A scale must also carry an id and a unit, since a rating
that cannot cite its scale is back to being a bare letter.

No scale is shipped as canonical. The examples in the tests are named
`example.*` on purpose: which thresholds are correct for a given metric is a
policy question this crate has no standing to settle, and baking one in would
repeat the `pue < 1.5` mistake with more ceremony.

Verified: 43 crate tests pass (7 new), no clippy warnings in the new file.
2026-08-27 10:41:37 -04:00