diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/eu_compliance.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/eu_compliance.rs index dcc1a108b..a2d51617f 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/eu_compliance.rs +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/eu_compliance.rs @@ -170,10 +170,12 @@ impl AiActReport { // ── ESPR ──────────────────────────────────────────────────────────── -/// ESPR Digital Product Passport report. +/// ESPR Digital Product Passport report, for a physical product: ESPR +/// applies to physical goods (Regulation (EU) 2024/1781, Art. 1(2)), such as +/// the hardware a service runs on, not to the service or its model. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EsprReport { - /// Product / model identifier. + /// Product identifier. pub product_name: String, /// EU energy label (`"A"`..`"G"`). pub energy_label: String, diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/lib.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/lib.rs index 9ea617b23..cebc1b8b3 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/lib.rs +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/lib.rs @@ -63,7 +63,7 @@ pub use eu_compliance::{ }; pub use passport::{ AiActCompliance, AiServicePassport, CarbonProfile, ComplianceRecord, CsrdCompliance, - EedCompliance, EnergyLabel, EnergyProfile, EsprCompliance, HardwareProfile, LocationInfo, + EedCompliance, EnergyLabel, EnergyProfile, HardwareProfile, LocationInfo, MeasurementMethod, ModelProfile, PassportBuilder, PassportCatalog, PassportError, PassportLabel, PricingInfo, }; diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/passport.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/passport.rs index 67f59db84..b32c46a4a 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/passport.rs +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-compliance/src/passport.rs @@ -11,7 +11,7 @@ //! method) — measurement method is mapped from //! [`jouleclaw_energy::Provenance`] //! 4. **Carbon profile** (SCI score, grid intensity, renewable share) -//! 5. **Compliance record** (EU AI Act, CSRD Scope 3, EED, ESPR DPP) +//! 5. **Compliance record** (EU AI Act, CSRD Scope 3, EED) //! 6. **Pricing** — optional, lightweight //! //! ## Sealing @@ -327,16 +327,18 @@ pub struct EedCompliance { pub renewable_energy_factor: f64, } -/// ESPR Digital Product Passport. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EsprCompliance { - /// URL of the hosted DPP. - pub passport_url: String, - /// Submitted to the EU registry. - pub registry_submitted: bool, -} - /// Aggregate compliance record. +/// +/// It carries no ESPR record. ESPR's digital product passport applies to +/// physical goods placed on the market or put into service (Regulation (EU) +/// 2024/1781, Art. 1(2)), and this passport describes a service. The +/// hardware a service runs on is a physical good: where a delegated act +/// requires it to have a product passport, that passport is the hardware's, +/// not this record's. A passport written before the record was removed still +/// reads, and its ESPR member is dropped. A seal taken over it was taken over +/// that member too, so [`AiServicePassport::sealed_hash`] of what is read +/// does not reproduce it: check such a seal against the bytes it was taken +/// over. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComplianceRecord { /// EU AI Act compliance. @@ -345,8 +347,6 @@ pub struct ComplianceRecord { pub csrd_scope_3: CsrdCompliance, /// EED compliance. pub eed: EedCompliance, - /// ESPR DPP compliance. - pub espr_dpp: EsprCompliance, } /// Pricing + settlement information. @@ -723,10 +723,6 @@ mod tests { waste_heat_recovery_pct: 45.0, renewable_energy_factor: 0.92, }, - espr_dpp: EsprCompliance { - passport_url: "https://example.com/dpp/llama".to_string(), - registry_submitted: true, - }, } } @@ -963,6 +959,23 @@ mod tests { assert_eq!(board[2].service_id, "svc-worst"); } + /// A passport written while it still carried an ESPR record reads; the + /// record is dropped, not carried forward as a claim, and leaves nothing + /// in what is sealed. + #[test] + fn an_older_passport_with_an_espr_record_still_reads() { + let p = build_sample("svc-old", 0.30, EnergyLabel::A); + let mut v = serde_json::to_value(&p).unwrap(); + v["compliance"]["espr_dpp"] = serde_json::json!({ + "passport_url": "https://example.com/dpp/llama", + "registry_submitted": true, + }); + let read: AiServicePassport = serde_json::from_value(v).expect("an older passport reads"); + let again = serde_json::to_value(&read).unwrap(); + assert!(again["compliance"].get("espr_dpp").is_none()); + assert_eq!(read.sealed_hash().unwrap(), p.sealed_hash().unwrap()); + } + #[test] fn pricing_default_uses_base_chain() { let p = PricingInfo::default();