From 58273f374e7668f78e03db8eb0a86a96fde94a23 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 22 Oct 2026 07:26:53 -0400 Subject: [PATCH] jouleclaw-compliance: no ESPR record on an AI service passport The AI Service Passport's compliance record carried an ESPR digital product passport with a bare `registry_submitted: bool`. ESPR applies to physical goods placed on the market or put into service (Regulation (EU) 2024/1781, Art. 1(2)); a passport here describes a service, so the record claimed a regime that cannot apply, under a flag nothing checked. It is removed. The hardware a service runs on is a physical good, and its passport, where a delegated act requires one, is the hardware's. A passport written with the record still reads, and the member is dropped. A seal taken over the old shape does not re-derive from what is read; the record's doc says to check it against the original bytes. EsprReport, a product-shaped report, now says it is for a physical product rather than "product / model". Co-Authored-By: Claude Opus 5.5 --- .../jouleclaw-compliance/src/eu_compliance.rs | 6 ++- .../crates/jouleclaw-compliance/src/lib.rs | 2 +- .../jouleclaw-compliance/src/passport.rs | 45 ++++++++++++------- 3 files changed, 34 insertions(+), 19 deletions(-) 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(); -- 2.54.0 (Apple Git-157)