compliance: publish the EU draft PUE bands as a sourced data pack, not as code #78
4 changed files with 112 additions and 0 deletions
|
|
@ -372,6 +372,43 @@ mod tests {
|
|||
assert_eq!(g.components.len(), 2, "components are kept so the grade re-derives");
|
||||
}
|
||||
|
||||
/// The published pack must actually load into this type and band correctly.
|
||||
/// A data file nobody parses is not a scale, it is a document.
|
||||
#[test]
|
||||
fn the_published_eu_pack_loads_and_bands() {
|
||||
let raw = include_str!("../../../../scales/v1/eu.eed.dr2026.pue.json");
|
||||
let scale: BandingScale = serde_json::from_str(raw).expect("pack deserializes");
|
||||
scale.validate().expect("a published pack must be able to rank");
|
||||
|
||||
assert_eq!(scale.scale_id, "eu.eed.dr2026.pue/1");
|
||||
assert_eq!(scale.unit, "milli_pue");
|
||||
assert_eq!(scale.direction, Direction::LowerIsBetter);
|
||||
// The last boundary is 1900, not the 2000 used by this file's own
|
||||
// example fixture — a detail worth pinning, since the two differ and
|
||||
// only one of them is sourced.
|
||||
assert_eq!(scale.thresholds[5], 1_900);
|
||||
|
||||
assert_eq!(scale.rate(1_100).unwrap().label, EnergyLabel::A);
|
||||
assert_eq!(scale.rate(1_150).unwrap().label, EnergyLabel::A);
|
||||
assert_eq!(scale.rate(1_901).unwrap().label, EnergyLabel::G);
|
||||
|
||||
// Every rating from the pack cites it, so a grade can be re-derived
|
||||
// against the published file rather than trusted.
|
||||
assert_eq!(scale.rate(1_400).unwrap().scale_id, "eu.eed.dr2026.pue/1");
|
||||
}
|
||||
|
||||
/// The pack ships PUE only. The instrument grades on PUE and WUE, so a
|
||||
/// composite built from this pack alone must report that it covers one axis
|
||||
/// — not quietly present a half-graded facility as graded.
|
||||
#[test]
|
||||
fn the_pack_does_not_pretend_to_cover_water() {
|
||||
let raw = include_str!("../../../../scales/v1/eu.eed.dr2026.pue.json");
|
||||
let scale: BandingScale = serde_json::from_str(raw).unwrap();
|
||||
let g = CompositeRating::combine(Combine::Worst, vec![scale.rate(1_100).unwrap()]).unwrap();
|
||||
assert!(g.covers(&["pue"]));
|
||||
assert!(!g.covers(&["pue", "wue"]), "one sourced axis is one axis");
|
||||
}
|
||||
|
||||
/// The function that did not exist: measurement in, band out.
|
||||
#[test]
|
||||
fn a_measurement_becomes_a_band() {
|
||||
|
|
|
|||
50
jouleclaw/scales/README.md
Normal file
50
jouleclaw/scales/README.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Banding scales
|
||||
|
||||
Published threshold sets for [`BandingScale`](../jouleclaw-rs/crates/jouleclaw-compliance/src/banding.rs).
|
||||
|
||||
## Why scales live here and not in the crate
|
||||
|
||||
`BandingScale::rate` turns a measurement into an A–G band. The crate ships the
|
||||
**shape** of a scale and no scale at all, deliberately: which thresholds are
|
||||
correct for a given metric is a policy question a library has no standing to
|
||||
settle. Baking one in would repeat the mistake the type was written to retire —
|
||||
`EedReport::is_compliant`'s `pue < 1.5 && renewable_pct >= 50.0`, two magic
|
||||
floats with no provenance, no version, and no way to say which instrument they
|
||||
encoded — only with more ceremony.
|
||||
|
||||
So a scale is **data**, published here with its sources beside it. `BandingScale`
|
||||
derives `Deserialize`, so a pack loads with `serde_json::from_str` and needs no
|
||||
crate code:
|
||||
|
||||
```rust
|
||||
let scale: BandingScale = serde_json::from_str(include_str!("…/eu.eed.dr2026.pue.json"))?;
|
||||
let rating = scale.rate(report.pue_milli())?;
|
||||
```
|
||||
|
||||
## What a pack must carry
|
||||
|
||||
Each `<scale_id>.json` is exactly a `BandingScale`. Each is accompanied by
|
||||
`<scale_id>.provenance.json` recording where the numbers came from, the status
|
||||
of that source, what the pack deliberately omits, and the reuse terms.
|
||||
|
||||
A pack with no provenance file is not publishable here. A threshold whose origin
|
||||
is unstated is the thing this directory exists to replace.
|
||||
|
||||
## The scale id is a version, and never edited in place
|
||||
|
||||
Ratings cite their `scale_id`, and `Rating::comparable_with` refuses to rank two
|
||||
ratings from different scales. That only works if an id means one fixed set of
|
||||
boundaries forever. When a source instrument changes, the result is a **new
|
||||
id** — never an edit to an existing file, which would silently re-band every
|
||||
grade already issued under it.
|
||||
|
||||
## Current packs
|
||||
|
||||
| scale | metric | source status |
|
||||
|---|---|---|
|
||||
| `eu.eed.dr2026.pue/1` | PUE, milli-units | **Draft** delegated regulation (revised 2026-07-02, not adopted) |
|
||||
|
||||
A grade produced from a draft instrument is not a regulatory determination, and
|
||||
the provenance file says so in those words. These packs exist so that a figure
|
||||
can be banded *against a stated source*; they do not make the source binding,
|
||||
and they do not make a self-declared input measured.
|
||||
7
jouleclaw/scales/v1/eu.eed.dr2026.pue.json
Normal file
7
jouleclaw/scales/v1/eu.eed.dr2026.pue.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"scale_id": "eu.eed.dr2026.pue/1",
|
||||
"metric": "pue",
|
||||
"unit": "milli_pue",
|
||||
"direction": "LowerIsBetter",
|
||||
"thresholds": [1150, 1250, 1350, 1500, 1700, 1900]
|
||||
}
|
||||
18
jouleclaw/scales/v1/eu.eed.dr2026.pue.provenance.json
Normal file
18
jouleclaw/scales/v1/eu.eed.dr2026.pue.provenance.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"scale_id": "eu.eed.dr2026.pue/1",
|
||||
"instrument": "Draft delegated regulation establishing an EU-wide scheme for rating the sustainability of data centres, with an electronic label",
|
||||
"instrument_status": "DRAFT — registered 2026-03-26, revised draft dated 2026-07-02. NOT adopted, NOT in the Official Journal.",
|
||||
"first_labels_expected": "2027-08-15",
|
||||
"graded_metrics_in_instrument": ["pue", "wue"],
|
||||
"metrics_in_this_pack": ["pue"],
|
||||
"omitted_deliberately": {
|
||||
"wue": "The instrument sets the A-G grade from PUE and WUE together, but this pack ships only the PUE bands, because only those were read from the instrument text. Inventing WUE boundaries to complete the set would be precisely the failure BandingScale exists to prevent: a threshold with no provenance, presented as if it had one. A composite grade built from this pack alone covers one axis, and CompositeRating::covers reports that it does."
|
||||
},
|
||||
"reuse": "Commission documents are reusable under Decision 2011/833/EU (CC BY 4.0). This pack restates threshold values only; it reproduces no instrument text.",
|
||||
"read_on": "2026-08-29",
|
||||
"caveats": [
|
||||
"A grade produced from this scale is NOT a regulatory determination. The instrument is a draft; its thresholds may change before adoption, and the scheme's own assurance requirements are not modelled here.",
|
||||
"The instrument permits measurement AND calculation methods, with an explicit fallback to well-founded assumptions and estimates. A PUE fed to this scale may therefore be self-declared. The scale bands whatever number it is given and says nothing about how that number was acquired — carry the acquisition class alongside it.",
|
||||
"This pack supersedes no earlier reading. If the adopted instrument differs, that is a NEW scale_id (eu.eed.dr2026.pue/2 or a successor), never an edit to this file: ratings cite the scale_id, so editing in place would silently re-band every grade already issued."
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue