Skip to content

feat(vault): model the Art. 77(1) passport scope - #239

Open
LKSNDRTMLKV wants to merge 2 commits into
feat/battery-import-templatesfrom
feat/art77-passport-scope
Open

feat(vault): model the Art. 77(1) passport scope#239
LKSNDRTMLKV wants to merge 2 commits into
feat/battery-import-templatesfrom
feat/art77-passport-scope

Conversation

@LKSNDRTMLKV

@LKSNDRTMLKV LKSNDRTMLKV commented Sep 4, 2026

Copy link
Copy Markdown
Member

Addresses #238 — the second half needs dpp-core and stays open. Stacked on feat/battery-import-templates (#237).

Engine-only, deliberatelydpp-core is under review elsewhere, so nothing
here touches it. That constraint decides how much of #238 this can close; see
What this cannot fix below.

The rule

Art. 77(1), verbatim from the OJ:

"From 18 February 2027 each LMT battery, each industrial battery with a
capacity greater than 2 kWh and each electric vehicle battery placed on the
market or put into service shall have an electronic record ('battery
passport')."

Five categories are defined (Art. 3(9), (11), (12), (13), (14)); this article
reaches three. domain::passport_scope is one pure function over values core
already owns, so lifting it into core later is a move rather than a rewrite.

Two qualifiers the article turns on, both easy to lose:

  • The threshold is energy, not charge. It says kWh. The Regulation's only
    definition of "rated capacity" — ampere-hours — is scoped "for the purposes
    of this Annex"
    and does not govern Art. 77(1). The comparison is against
    ratedCapacityKwh, never nominalCapacityAh.
  • No rechargeable qualifier. Arts. 7 and 8 both say "rechargeable
    industrial batteries with a capacity greater than 2 kWh". Art. 77(1) does not.

Which way to be wrong

An undeclared capacity is treated as in scope. Exempting on an unknown is
the error that silently switches off a statutory gate; including on an unknown
is friction an operator can end by declaring the capacity, and the note tells
them so. Deriving energy from nominalVoltageV × nominalCapacityAh (both
required) was considered and rejected for the exempting direction — an inference
about a battery is not the ground on which to stop asking for content the law
requires. An unrecognised BatteryType (the enum is #[non_exhaustive]) is
handled the same way, and logs.

What this cannot fix

check_mandatory_content runs inside dpp-core's Passport::transition_to on
first publish. The engine calls that; it cannot skip it. So the concrete
over-demand in #238 — an industrial battery at or below 2 kWh being asked for
content the article exempts — is still present, and needs a core change.

Measured, not assumed: an industrial battery at 1.5 kWh reports
passportScope: voluntary and still returns 34 blockers. Rather than
leave that unexplained, the note says it outright ("this node still applies the
category content gate, which is stricter than the article requires here"), and
a_battery_outside_article_77_is_voluntary_and_still_gated pins both halves —
so if core ever narrows the gate, the test fails and the note goes with it.

The other half of #238 is closed: a portable or SLI passport is now named
voluntary instead of passing silently.

Verified live

batteryType ratedCapacityKwh obligation
ev required
lmt required
industrial 64 required
industrial 2.0 voluntary (article says greater than)
industrial 1.5 voluntary
industrial required + note on how to leave scope
portable voluntary
starting-lighting-ignition voluntary

just check green (971 tests), just openapi-check green.

Not core's PassportObligation

dpp_domain::instrument::PassportObligation already exists and answers a
different question, so this type is deliberately named PassportScope:

  • Core — an act and a product group: does Reg. (EU) 2023/1542 create a
    passport duty for batteries, and from when (Required/NotRequired/
    DisplacedBy).
  • Hereone record: given that the duty exists, does Art. 77(1) reach
    this battery, whose type and capacity decide it.

They compose. Naming both "obligation" would invite a reader to assume one was
the other — the same two-names-one-concept mistake the sector/productGroup
rename was paid for.

Build integrity

Verified against published dpp-core 0.19.0, with the local
[patch.crates-io] override removed, because dpp-core's working tree is one
commit ahead of the published tag and is being edited by someone else. 971 tests
pass either way; CI builds the published pin, so that is the run that counts.
Nothing in this branch touches dpp-core.

Nothing read Art. 77(1)'s scope, so the node could not tell a battery it must passport from one it need not: a portable or SLI battery published with no mandatory content at all while claiming to be a battery passport, and an industrial battery at or below 2 kWh — which the article exempts — was still asked for the full category content. The scope is now a predicate over the values core already owns and is reported on lint beside the blockers, including the part the engine cannot fix: the content gate lives in core's transition_to and fires whatever the scope says, so a voluntary passport is still gated and the note admits it rather than leaving the operator to wonder.
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 25 complexity · 5 duplication

Metric Results
Complexity 25
Duplication 5

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

`dpp_domain::instrument::PassportObligation` already exists and answers a different question — whether an act creates a passport duty for a product group, and from when — while this one answers whether Art. 77(1) reaches a given record; two types with one name in adjacent crates is the mistake the sector/productGroup rename was paid for, so the engine type is PassportScope and the module header says why they compose rather than compete.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant