From ba249d0276e982c409880c2e76fc7a8b0d1d932c Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Thu, 13 Aug 2026 21:48:54 +0200 Subject: [PATCH] docs: bring docs and changelog up to 0.17.0 --- CHANGELOG.md | 76 +++++++++++++++++++++++-- crates/dpp-domain/README.md | 17 ++++-- docs/architecture/DATA-MODEL.md | 97 ++++++++++++++++++-------------- docs/architecture/PLUGIN-HOST.md | 2 +- docs/governance/RELEASE.md | 16 ++++-- docs/regulatory/COMPLIANCE.md | 3 +- justfile | 5 +- 7 files changed, 156 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c78687e..7777b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -184,11 +184,14 @@ This file was started retroactively on 2026-07-03 at v0.4.0; entries for industrial battery with no meaningful cycle figure unrepresentable. The constraint belongs in a category-conditional rule, not in the schema. - `BatteryData` also gains three fields (below) and is not `#[non_exhaustive]`, - so struct-literal construction must be updated. The `v2.5.0 → v2.6.0` lens is - an identity: a relaxation plus optional additions cannot strand a record that - already validated, which is why this hop passes through where the `v2.4.0` - one refuses. + `BatteryData` is not `#[non_exhaustive]`, so **every** struct literal + constructing it must be updated. Across this release the type goes from 37 + fields to 68 — the Annex XIII point 1, 2, 3 and 4 tiers, plus the four Annex + VI Part A identity fields — and none of the 37 is removed. Struct-update + syntax (`..base`) is unaffected; exhaustive literals are not. The + `v2.5.0 → v2.6.0` lens is an identity: a relaxation plus optional additions + cannot strand a record that already validated, which is why this hop passes + through where the `v2.4.0` one refuses. - **`BatteryData.battery_type` is required and closed.** Annex VI Part A point 2, made public by Annex XIII point 1(a), lists the battery category as @@ -428,6 +431,51 @@ This file was started retroactively on 2026-07-03 at v0.4.0; entries for EU regulation changes; this changes when GS1 or IDTA publishes, which is why it sits apart from the passport model rather than inside it. +### Changed + +- **The battery AAS submodel projects 17 fields it previously dropped.** What an + AAS consumer receives for a battery changes accordingly. Added: the four Annex + VI Part A identity fields, the two carbon-footprint ruleset provenance fields, + `usableExtinguishingAgent`, `hazardSymbol`, `hazardousSubstances`, the two + reference-test conditions, `commercialWarrantyPeriodMonths`, + `recycledContentReportingYear`, `placedOnMarketDate`, and the three + temperature ranges (each a collection of `minC`/`maxC`, since the two numbers + mean nothing apart). + + `carbonFootprintClass` is the one that was actively wrong rather than merely + absent: the mapper emitted the bare label while the type's own doc comment + says it is "meaningless without the two provenance fields — the same label + denotes different thresholds under different revisions of the scale". An + exported class with no ruleset is not a weaker claim than a qualified one, it + is an unfalsifiable one. The class and its ruleset now travel together. + + Ten schema properties still do not project, and the mapper now says why for + each: seven are documents, contacts or time series that a technical-data + snapshot deliberately excludes; `expectedLifetime` and `stateOfHealth` are a + recorded gap rather than a decision. + +- **`carbonFootprintClass` and `dueDiligenceUrl` are barred from a battery + passport for now.** The Commission's guidance marks both *"not to be + filled/displayed as of February 2027 — format still to be specified in the + upcoming implementing act"*, and the per-category table had no row for either. + Both are now `NotApplicable` for every covered category, the same class + `ratedCapacityAh` already carried for the same words. Advisory rather than + blocking: the publish gate reads only the mandatory set, so a passport + carrying one is flagged, not refused. + + Not to be confused with disclosure. `dueDiligenceUrl` is Annex XIII point 1(d) + and therefore *public* once it is filled; whether a field may be seen and + whether it may be filled are different axes, and this release changes both in + opposite directions for the same field. + +- **All 71 guidance data points are now accounted for.** The per-category table + previously stated that the difference between its length and the guidance's 71 + rows was unclassified. Each row has since been walked against the model and the + module records where every one went: envelope fields, one-row-per-block + groupings for the point 4 tiers, rows the guidance defers, and rows restated + elsewhere in the annex. Four omissions found and closed — see the Breaking + entry on the identity data points. + ### Fixed - **Battery schema `v2.6.0` now declares four Annex VI Part A fields the type @@ -476,6 +524,24 @@ This file was started retroactively on 2026-07-03 at v0.4.0; entries for version from the catalog instead of naming one, so it cannot quietly end up asserting against a superseded schema. + The same guard now exists for textile, the other product group with a real + model rather than a stub. A mechanical struct-versus-schema sweep across all + ten sectors found battery the only one that had drifted; the other nine were + at exact parity. + +- **A misspelt `x-disclosure` class published the field it was meant to + withhold.** `SectorAccessPolicy::from_schema` matches four known tokens and + drops anything else, so `"restrcted"` produced no map entry and the field fell + through to the `Public` default. One transposed character was enough, and the + coverage tests could not catch it because they asserted only that *some* + string was present. + + Both coverage tests now validate the token against the four the constructor + accepts, so a typo fails the build rather than the audience check. Raising the + default instead does not work: the policy applies to the whole document, and + the passport envelope's public fields are declared in no sector schema, so a + non-public default would erase them. + ## [0.16.0] - 2026-08-07 ### Added diff --git a/crates/dpp-domain/README.md b/crates/dpp-domain/README.md index 2b02d86..2a9d610 100644 --- a/crates/dpp-domain/README.md +++ b/crates/dpp-domain/README.md @@ -25,20 +25,25 @@ use dpp_domain::catalog::SectorCatalog; use dpp_domain::Audience; use serde_json::json; -// Sector metadata is data, not code: regime, status, retention and per-field -// disclosure all come from the catalog manifests. +// Sector metadata is data, not code: regime, status and retention all come +// from the catalog manifests. let catalog = SectorCatalog::new(); let battery = catalog.get("battery").expect("battery is in the catalog"); assert_eq!(battery.key, "battery"); -// The disclosure classes declared in that manifest are what the filter applies. -let policy = SectorAccessPolicy::from_catalog(&catalog, "battery") - .expect("battery declares a disclosure policy"); +// Disclosure classes come from the schema *version* a passport declares, +// not from one unversioned map. That is what lets a published passport keep +// the classification its signatures were taken under: reclassifying a field +// is a new schema version, and an older passport goes on being filtered by +// the version it was validated against. +let policy = SectorAccessPolicy::for_schema_version("battery", &battery.current_schema_version) + .expect("the current battery schema classifies every property"); let full = json!({ "productName": "EcoCell", "stateOfHealthPct": 87.5 }); let public = filter_by_audience(&full, &policy, Audience::Public); -// State of health is per-item data — withheld from the public audience. +// State of health is Annex XIII point 4 — data about one individual battery, +// reserved to holders of a legitimate interest and withheld from the public. assert_eq!(public.filtered_data["productName"], "EcoCell"); assert!(public.filtered_data.get("stateOfHealthPct").is_none()); ``` diff --git a/docs/architecture/DATA-MODEL.md b/docs/architecture/DATA-MODEL.md index a9e17f0..2b6b5f0 100644 --- a/docs/architecture/DATA-MODEL.md +++ b/docs/architecture/DATA-MODEL.md @@ -2,7 +2,9 @@ This document defines the canonical data structures for all Digital Product Passports. It is the authoritative reference for domain types in `dpp-domain`, JSON Schema fields, and Verifiable Credential payloads. -> **Last updated**: 2026-05-29 (aligned to actual `Passport` struct, `BatteryData` v2.0.0, `TextileData` v1.1.0) +> **Last updated**: 2026-08-13 (aligned to `BatteryData` v2.6.0, `TextileData` v1.2.0, +> `ElectronicsData` v1.2.0). Field-level detail lives in the types and schemas; +> this document describes shape and rules, not every column. --- @@ -99,7 +101,7 @@ Elements of `Passport.materials` — bill of materials entries. |---|---|---| | `battery` | `battery_type` | Battery Reg. 2023/1542 Art. 1(3) — closed, five categories, required | | `steel` | `product_category` | `"flat"` / `"long"` / … | -| `electronics` | `product_category` | `"smartphone"` / `"laptop"` / … | +| `electronics` | `product_category` | `"smartphone"` / `"other-mobile-phone"` / `"cordless-phone"` / `"tablet"` — closed, Reg. (EU) 2023/1670 Art. 1(1) | | `unsold-goods` | `product_category` | `"apparel"` / `"footwear"` / … | | `furniture` | `product_type` | — | | `tyre` | `tyre_class` | `"C1"` / … | @@ -119,54 +121,64 @@ Sector-specific data is stored in `SectorData`, a tagged enum. Each variant has **Serde**: `SectorData` uses `rename_all = "camelCase"` with internally-tagged format. -### 4.1 Battery Sector (`BatteryData`) — v2.0.0 +### 4.1 Battery Sector (`BatteryData`) — v2.6.0 -Source: EU Battery Regulation (EU) 2023/1542, Annex XIII. Battery DPP mandatory from 18 Feb 2027. +Source: EU Battery Regulation (EU) 2023/1542. Battery DPP mandatory from +18 Feb 2027. -**Required fields** (6): +**This section deliberately does not list every field.** `BatteryData` carries +68, and a hand-maintained copy of that list is the same drift that let four +Annex VI Part A fields sit in the type for seven schema versions with no schema +property to validate against. The authoritative pair is: -| Field | Rust Type | JSON name | Reg. Source | -|---|---|---|---| -| `gtin` | `String` (14 digits) | `"gtin"` | GS1 / ESPR | -| `battery_chemistry` | `String` | `"batteryChemistry"` | Art. 13(1)(a) | -| `nominal_voltage_v` | `f64` | `"nominalVoltageV"` | Art. 13(1)(b) | -| `nominal_capacity_ah` | `f64` | `"nominalCapacityAh"` | Art. 13(1)(c) | -| `expected_lifetime_cycles` | `u32` | `"expectedLifetimeCycles"` | Art. 10(1) | -| `co2e_per_unit_kg` | `f64` | `"co2ePerUnitKg"` | Art. 7(1) | +- `crates/dpp-domain/src/domain/sector/data/battery.rs` — the type, with a + per-field regulatory citation on each doc comment. +- `crates/dpp-domain/schemas/battery/v2.6.0.json` — the wire contract, with an + `x-disclosure` class on every property. `additionalProperties` is `false`, and + a test asserts the two agree field-for-field. + +**Required** (6, and the only ones a passport cannot omit at any category): +`gtin`, `batteryChemistry`, `nominalVoltageV`, `nominalCapacityAh`, +`co2ePerUnitKg`, `batteryType`. Everything else is `Option` — not laxity, but +because the obligations are **per category**: a field mandatory for an +electric-vehicle battery may be "not to be filled/displayed" for an LMT one. +That constraint lives in `dpp_rules::batteries::passport_content`, which the +publish gate enforces, rather than in the schema. -**Optional fields** (21 — all `Option`, `skip_serializing_if = "Option::is_none"`): +**Shape.** The fields group by the Annex XIII tier that governs who may see +them, which is also what `x-disclosure` records: -| Field | Rust Type | JSON name | Reg. Source | +| Tier | Annex XIII | Audience | Examples | |---|---|---|---| -| `recycled_content_cobalt_pct` | `Option` | `"recycledContentCobaltPct"` | Art. 8 | -| `recycled_content_lithium_pct` | `Option` | `"recycledContentLithiumPct"` | Art. 8 | -| `recycled_content_nickel_pct` | `Option` | `"recycledContentNickelPct"` | Art. 8 | -| `recycled_content_lead_pct` | `Option` | `"recycledContentLeadPct"` | Art. 8 (lead-acid) | -| `state_of_health_pct` | `Option` | `"stateOfHealthPct"` | Art. 14 | -| `rated_capacity_kwh` | `Option` | `"ratedCapacityKwh"` | Art. 13(1)(d) | -| `rated_energy_wh` | `Option` | `"ratedEnergyWh"` | Art. 13(1)(d) | -| `carbon_footprint_class` | `Option` | `"carbonFootprintClass"` | Art. 7(2) — A–E | -| `due_diligence_url` | `Option` | `"dueDiligenceUrl"` | Art. 47-52 | -| `cathode_material` | `Option>` | `"cathodeMaterial"` | Annex XIII §4 | -| `anode_material` | `Option>` | `"anodeMaterial"` | Annex XIII §4 | -| `electrolyte_material` | `Option>` | `"electrolyteMaterial"` | Annex XIII §4 | -| `critical_raw_materials` | `Option>` | `"criticalRawMaterials"` | EU CRM Act 2024/1252 | -| `disassembly_instructions_url` | `Option` | `"disassemblyInstructionsUrl"` | Annex XIII §6 | -| `soh_methodology` | `Option` | `"sohMethodology"` | Art. 14(2) | -| `operating_temp_min_c` | `Option` | `"operatingTempMinC"` | Annex XIII | -| `operating_temp_max_c` | `Option` | `"operatingTempMaxC"` | Annex XIII | -| `battery_weight_kg` | `Option` | `"batteryWeightKg"` | Annex XIII | -| `battery_type` | `String` (required since v2.5.0) | `"batteryType"` | Art. 1(3), closed enum: portable, industrial, ev, lmt, starting-lighting-ignition — mandatory public content (Annex VI Part A pt 2 via Annex XIII pt 1(a)) | -| `round_trip_efficiency_pct` | `Option` | `"roundTripEfficiencyPct"` | Art. 10 — at 50% SoC | -| `internal_resistance_mohm` | `Option` | `"internalResistanceMohm"` | Art. 10 — at 50% SoC | +| `public` | point 1, incl. Annex VI Part A via 1(a) | anyone | chemistry, voltages, recycled content, place and date of manufacture | +| `restricted` | point 2 | authorities **and** legitimate interest | cathode/anode/electrolyte composition, dismantling, safety measures | +| `conformity` | point 3 | authorities only | test report results | +| `individual` | point 4 | legitimate interest only | measured performance, state of health, status, use history | + +Point 4 is per **item** rather than per model, so those fields nest into +`DynamicPerformance`, `StateOfHealth`, `UsageHistory` and `ExpectedLifetime` +rather than flattening — a declared model figure and a measured one are +different claims and must not sit side by side under near-identical names. + +**Legacy fields.** `state_of_health_pct`, `round_trip_efficiency_pct` and +`internal_resistance_mohm` are superseded but retained: a stored record keeps +its value under the name it was written with. See the type's own doc comment +for the rule and for why deletion is reserved for the cases where keeping the +field is itself the defect. **Helper types**: -- `MaterialComposition { name: String, weight_pct: f64, cas_number: Option }` -- `CriticalRawMaterial { name: String, cas_number: Option, weight_grams: Option, country_of_origin: Option }` +- `MaterialComposition { name, weight_pct, cas_number }` +- `CriticalRawMaterial { name, cas_number, weight_grams, country_of_origin }` +- `HazardousSubstance { name, cas_number, concentration_pct }` +- `TemperatureRange { min_c, max_c }` + +Schemas: `schemas/battery/v{1.0.0, 2.0.0 … 2.6.0}.json`. Older versions stay +registered so a passport validated against one remains verifiable, and each +carries its own disclosure classes — which is what stops a reclassification +changing the bytes served for an already-published passport. -Schema: `schemas/battery/v2.0.0.json` -### 4.2 Textile Sector (`TextileData`) — v1.1.0 +### 4.2 Textile Sector (`TextileData`) — v1.2.0 Source: ESPR Working Group on Textiles. Delegated act adoption anticipated ~Q2 2027, compliance ~2028–2029. @@ -246,4 +258,7 @@ Schemas follow semver. The `VersionedSchemaRegistry` in `dpp-domain` discovers a | Minor (`1.x.0`) | New optional fields; provisional -> strict | Yes | | Major (`x.0.0`) | Field renamed, type changed, or removed | No | -Current schemas: battery/v2.0.0, textile/v1.0.0, textile/v1.1.0, unsold-goods/v1.0.0, steel/v1.0.0. +Current schemas: 28 embedded versions across 11 sectors — see +`crates/dpp-domain/src/schemas/embedded.rs` for the registered list. Every one +is reachable at runtime; a passport is validated against the version it +declares, not against the newest. diff --git a/docs/architecture/PLUGIN-HOST.md b/docs/architecture/PLUGIN-HOST.md index c73677c..f4ced55 100644 --- a/docs/architecture/PLUGIN-HOST.md +++ b/docs/architecture/PLUGIN-HOST.md @@ -103,7 +103,7 @@ All ten plugins run on the SDK (`dpp-plugin-sdk` + `export_plugin!`): | Plugin | Sector | Schema | ABI | |---|---|---|---| -| `sector-battery` | battery | `schemas/battery/v{1.0.0,2.0.0}.json` | SDK (`DppSectorPlugin`) | +| `sector-battery` | battery | `schemas/battery/v{1.0.0, 2.0.0 … 2.6.0}.json` | SDK (`DppSectorPlugin`) | | `sector-textile` | textile, unsold-goods | `schemas/textile/*`, `unsold-goods/*` | SDK (`DppSectorPlugin`) | | `sector-steel` | steel | `schemas/steel/v1.0.0.json` | SDK (`DppSectorPlugin`) | | `sector-electronics`, `-construction`, `-tyre`, `-toy`, `-aluminium`, `-furniture`, `-detergent` | resp. | `schemas/{sector}/v1.0.0.json` | SDK (`DppSectorPlugin`) | diff --git a/docs/governance/RELEASE.md b/docs/governance/RELEASE.md index 36524a3..8d81431 100644 --- a/docs/governance/RELEASE.md +++ b/docs/governance/RELEASE.md @@ -55,11 +55,17 @@ order: 2. `dpp-plugin-traits` (no workspace dependencies) 3. `dpp-crypto` (no workspace dependencies) 4. `dpp-calc` (no workspace dependencies) -5. `dpp-domain` (depends on dpp-rules) -6. `dpp-registry` (depends on dpp-domain) -7. `dpp-digital-link`, `dpp-aas` (depend on dpp-domain) -8. `dpp-vc` (depends on dpp-domain + dpp-crypto) -9. `dpp-plugin-sdk` (depends on dpp-plugin-traits + dpp-rules) +5. `dpp-vocab` (no workspace dependencies — a leaf, by design) +6. `dpp-domain` (depends on dpp-rules) +7. `dpp-registry` (depends on dpp-domain) +8. `dpp-digital-link` (depends on dpp-domain) +9. `dpp-aas` (depends on dpp-domain + dpp-vocab) +10. `dpp-vc` (depends on dpp-domain + dpp-crypto) +11. `dpp-plugin-sdk` (depends on dpp-plugin-traits + dpp-rules) + +`dpp-vocab` is new in 0.17.0 and has never been published. It has no baseline on +crates.io, which is why `just semver` excludes it — delete that exclusion once +this release has shipped it once. `dpp-tests` is `publish = false` and is not published, but it is still lockstep- versioned with everything else. `cargo-release` handles publish ordering diff --git a/docs/regulatory/COMPLIANCE.md b/docs/regulatory/COMPLIANCE.md index 088aa24..a30a9cc 100644 --- a/docs/regulatory/COMPLIANCE.md +++ b/docs/regulatory/COMPLIANCE.md @@ -12,7 +12,7 @@ Passports mandated by: | Regulation | Reference | Status | dpp-core Coverage | |---|---|---|---| | ESPR | Regulation (EU) 2024/1781 | In force | Core passport model, per-audience field disclosure, schema validation | -| Battery Regulation | Regulation (EU) 2023/1542 | In force, DPP deadline Feb 2027 | Battery schemas (v1.0.0, v2.0.0), sector plugin | +| Battery Regulation | Regulation (EU) 2023/1542 | In force, DPP deadline Feb 2027 | Battery schemas (v1.0.0, v2.0.0 … v2.6.0), sector plugin, per-category content rules | | Textile (anticipated) | ESPR delegated act (draft) | See timeline note below | Textile schemas (v1.0.0, v1.1.0), sector plugin | | CBAM | Regulation (EU) 2023/956 | In force | Steel schema (v1.0.0), embedded emissions fields | @@ -40,6 +40,7 @@ Each JSON schema under `schemas/` is traceable to its regulatory basis: |---|---|---|---| | `battery/v1.0.0.json` | 2023/1542 | Art. 77, Annex XIII | Minimum viable battery passport | | `battery/v2.0.0.json` | 2023/1542 | Art. 77, Annex XIII | Extended fields for carbon footprint | +| `battery/v2.6.0.json` | 2023/1542 | Annex VI Part A, Annex XIII points 1–4 | Current. All four disclosure tiers, per-property `x-disclosure` | | `textile/v1.0.0.json` | ESPR 2024/1781 | Art. 9-10 (framework) | Baseline textile passport | | `textile/v1.1.0.json` | ESPR 2024/1781 | Art. 9-10 + anticipated delegated act | Adds fibre composition, durability, microplastics | | `unsold-goods/v1.0.0.json` | ESPR 2024/1781 | Art. 25 / Annex VII | Unsold-goods destruction-ban compliance | diff --git a/justfile b/justfile index 6653938..e71b8d1 100644 --- a/justfile +++ b/justfile @@ -56,7 +56,10 @@ audit: # position, so the tool waives every lint. Measured on this workspace at # 0.16.0 -> 0.17.0: **0 checks run, 253 skipped, "no semver update required"** — # green because it stopped looking. With `--release-type patch` the same tree -# runs 223 checks and reports all five real removals. +# runs 223 checks per crate and reports every real break — seven of them on the +# merged 0.17.0 tree, across five lints: a removed const, enum, enum variant, +# module and struct field, plus a newly `#[deprecated]` method and the fields +# `BatteryData` gained (breaking for exhaustive struct literals). # # So this answers "what broke since the last published release", and gives the # same answer before and after a version bump. A failure is not a defect by