jouleclaw-compliance: no ESPR record on an AI service passport #441

Merged
dcharlot merged 1 commit from fix/passport-no-espr into main 2026-11-01 09:38:43 -05:00
3 changed files with 34 additions and 19 deletions

View file

@ -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,

View file

@ -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,
};

View file

@ -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();