@tracepass/dpp-validate 0.11.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vendor/battery-applicability.d.ts +107 -0
- package/dist/vendor/battery-applicability.d.ts.map +1 -0
- package/dist/vendor/battery-applicability.js +390 -0
- package/dist/vendor/battery-applicability.js.map +1 -0
- package/dist/vendor/condition-profile.d.ts +29 -0
- package/dist/vendor/condition-profile.d.ts.map +1 -0
- package/dist/vendor/condition-profile.js +47 -0
- package/dist/vendor/condition-profile.js.map +1 -0
- package/dist/vendor/publish-gate.d.ts +54 -8
- package/dist/vendor/publish-gate.d.ts.map +1 -1
- package/dist/vendor/publish-gate.js +105 -31
- package/dist/vendor/publish-gate.js.map +1 -1
- package/dist/verdict.d.ts +3 -2
- package/dist/verdict.d.ts.map +1 -1
- package/dist/verdict.js +42 -7
- package/dist/verdict.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Battery field-applicability predicate — the SINGLE source of truth for
|
|
3
|
+
* "does this Annex XIII field apply to *this* battery?", consumed by both:
|
|
4
|
+
*
|
|
5
|
+
* - the compliance verdict engine (rules.ts) — flags a filled field that
|
|
6
|
+
* doesn't apply, or warns when applicability can't be determined; and
|
|
7
|
+
* - the passport editor UI (PassportEditor.tsx) — soft-collapses
|
|
8
|
+
* not-applicable fields so a reviewer isn't asked for data the
|
|
9
|
+
* regulation doesn't require for their battery type.
|
|
10
|
+
*
|
|
11
|
+
* Source: DG GROW battery-passport webinar 27 May 2026, slide 36
|
|
12
|
+
* (per-category applicability matrix) + the cross-reference/scope-gate
|
|
13
|
+
* slides; Reg. (EU) 2023/1542 Art. 14(1) + Annex VII Parts A and B.
|
|
14
|
+
* Analysis: knowledgebase/regulations/dpp-battery-webinar-2026-05.md.
|
|
15
|
+
* Spec home: knowledgebase/regulations/dpp-conditional-rules.md.
|
|
16
|
+
*
|
|
17
|
+
* Design contract — the SAFETY rule that makes A safe:
|
|
18
|
+
* "unknown" (a gate's trigger field is absent) ⇒ treat as APPLIES for the
|
|
19
|
+
* UI (show the field) and as `unverifiable_conditional` for the verdict
|
|
20
|
+
* (warn, never assert). We NEVER hide or suppress a field on uncertainty —
|
|
21
|
+
* a mis-classified battery must never lose a legally-required field.
|
|
22
|
+
*
|
|
23
|
+
* Triggers:
|
|
24
|
+
* - batteryCategory — a real template field in battery.json (enum: LMT |
|
|
25
|
+
* EV | industrial_gt_2kwh | industrial_lte_2kwh | SLI
|
|
26
|
+
* | portable). Read from `passport.fields`.
|
|
27
|
+
* - hasBMS, rechargeable, externalStorageOnly, isStationaryBess — tri-state
|
|
28
|
+
* booleans on `passport.batteryProfile` (a separate
|
|
29
|
+
* block, NOT template fields — keeps the template's
|
|
30
|
+
* field count intact). Absent ⇒ the dependent gate
|
|
31
|
+
* resolves to "unknown" (show + warn), not a guess.
|
|
32
|
+
*
|
|
33
|
+
* Pure + IO-free → unit-tested in tests/compliance/battery-applicability.test.ts.
|
|
34
|
+
*/
|
|
35
|
+
import type { Passport } from "@tracepass/dpp-types";
|
|
36
|
+
import { IN_SCOPE_BATTERY_CATEGORIES } from "./battery-scope.js";
|
|
37
|
+
export type Applicability = "applies" | "not_applicable" | "unknown";
|
|
38
|
+
/** Battery categories that are *in scope* for a battery passport at all.
|
|
39
|
+
* Re-exported from the shared definition so this module's consumers keep
|
|
40
|
+
* importing it from here; the passport-level scope gate is rules.ts BAT-1. */
|
|
41
|
+
export { IN_SCOPE_BATTERY_CATEGORIES };
|
|
42
|
+
export interface FieldGate {
|
|
43
|
+
/** Battery template field keys this gate governs. */
|
|
44
|
+
keys: string[];
|
|
45
|
+
/** Annex XIII / deck reference, for the verdict citation. */
|
|
46
|
+
article: string;
|
|
47
|
+
/** One-line reason, surfaced in the verdict + editor tooltip. */
|
|
48
|
+
reason: string;
|
|
49
|
+
/** Extra remediation, for gates that turn on the stationary-storage flag. */
|
|
50
|
+
fixHint?: string;
|
|
51
|
+
/** Decide applicability from the resolved trigger values. */
|
|
52
|
+
decide(t: Triggers): Applicability;
|
|
53
|
+
}
|
|
54
|
+
interface Triggers {
|
|
55
|
+
category: string | undefined;
|
|
56
|
+
hasBMS: boolean | undefined;
|
|
57
|
+
rechargeable: boolean | undefined;
|
|
58
|
+
externalStorageOnly: boolean | undefined;
|
|
59
|
+
isStationaryBess: boolean | undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The conditional gates, one per applicability rule from the deck. Order
|
|
63
|
+
* doesn't matter — each governs a disjoint set of field keys.
|
|
64
|
+
*/
|
|
65
|
+
export declare const BATTERY_FIELD_GATES: FieldGate[];
|
|
66
|
+
/** Every field key under any gate (the conditional set). */
|
|
67
|
+
export declare const GATED_FIELD_KEYS: ReadonlySet<string>;
|
|
68
|
+
/**
|
|
69
|
+
* Applicability for a single battery field key. Ungated keys are always
|
|
70
|
+
* "applies". This is the granular accessor the editor uses per-field.
|
|
71
|
+
*/
|
|
72
|
+
export declare function fieldApplicability(passport: Passport, key: string): Applicability;
|
|
73
|
+
/**
|
|
74
|
+
* Full map for the gated fields only (callers default everything else to
|
|
75
|
+
* "applies"). The verdict engine iterates this to emit findings; the
|
|
76
|
+
* editor reads it once per render and looks up per field.
|
|
77
|
+
*
|
|
78
|
+
* Returns an empty map for non-battery passports — `category` here is the
|
|
79
|
+
* template category, NOT batteryCategory; callers pass it so this is a
|
|
80
|
+
* no-op outside battery and the predicate stays battery-scoped.
|
|
81
|
+
*/
|
|
82
|
+
/** @deprecated Use `categoryFieldApplicability` instead. Kept for backward compat. */
|
|
83
|
+
export declare function batteryFieldApplicability(passport: Passport, category: string): Record<string, Applicability>;
|
|
84
|
+
/** Look up the gate metadata (article + reason) for a gated field key. */
|
|
85
|
+
export declare function gateForKey(key: string): FieldGate | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* CATEGORY_FIELD_GATES — generalised gate registry, keyed by template category.
|
|
88
|
+
*
|
|
89
|
+
* Phase 1: battery only. Other categories gain entries when their conditional
|
|
90
|
+
* rules are encoded. Battery gates are unchanged from BATTERY_FIELD_GATES; the
|
|
91
|
+
* two names refer to the same array.
|
|
92
|
+
*
|
|
93
|
+
* Callers that need to enumerate gates for a category use this registry.
|
|
94
|
+
* The battery-specific `batteryFieldApplicability` is now a thin wrapper.
|
|
95
|
+
*/
|
|
96
|
+
export declare const CATEGORY_FIELD_GATES: Record<string, FieldGate[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Compute the applicability map for all gated fields of any category.
|
|
99
|
+
* Returns {} for categories with no gates (safe to iterate).
|
|
100
|
+
*
|
|
101
|
+
* Replaces the battery-only early return in `batteryFieldApplicability` with a
|
|
102
|
+
* generic path: every category that has gates in CATEGORY_FIELD_GATES is handled,
|
|
103
|
+
* everything else is a no-op. `batteryFieldApplicability` is kept as a
|
|
104
|
+
* backward-compat alias.
|
|
105
|
+
*/
|
|
106
|
+
export declare function categoryFieldApplicability(passport: Passport, category: string): Record<string, Applicability>;
|
|
107
|
+
//# sourceMappingURL=battery-applicability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"battery-applicability.d.ts","sourceRoot":"","sources":["../../src/vendor/battery-applicability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AAEjE,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,gBAAgB,GAAG,SAAS,CAAC;AAErE;;+EAE+E;AAC/E,OAAO,EAAE,2BAA2B,EAAE,CAAC;AAUvC,MAAM,WAAW,SAAS;IACxB,qDAAqD;IACrD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,MAAM,CAAC,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;CACpC;AAED,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,YAAY,EAAE,OAAO,GAAG,SAAS,CAAC;IAClC,mBAAmB,EAAE,OAAO,GAAG,SAAS,CAAC;IACzC,gBAAgB,EAAE,OAAO,GAAG,SAAS,CAAC;CACvC;AAsED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,EA2L1C,CAAC;AAQF,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAA+B,CAAC;AAEjF;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CAIjF;AAED;;;;;;;;GAQG;AACH,sFAAsF;AACtF,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAE/B;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE7D;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAE5D,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAU/B"}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Battery field-applicability predicate — the SINGLE source of truth for
|
|
3
|
+
* "does this Annex XIII field apply to *this* battery?", consumed by both:
|
|
4
|
+
*
|
|
5
|
+
* - the compliance verdict engine (rules.ts) — flags a filled field that
|
|
6
|
+
* doesn't apply, or warns when applicability can't be determined; and
|
|
7
|
+
* - the passport editor UI (PassportEditor.tsx) — soft-collapses
|
|
8
|
+
* not-applicable fields so a reviewer isn't asked for data the
|
|
9
|
+
* regulation doesn't require for their battery type.
|
|
10
|
+
*
|
|
11
|
+
* Source: DG GROW battery-passport webinar 27 May 2026, slide 36
|
|
12
|
+
* (per-category applicability matrix) + the cross-reference/scope-gate
|
|
13
|
+
* slides; Reg. (EU) 2023/1542 Art. 14(1) + Annex VII Parts A and B.
|
|
14
|
+
* Analysis: knowledgebase/regulations/dpp-battery-webinar-2026-05.md.
|
|
15
|
+
* Spec home: knowledgebase/regulations/dpp-conditional-rules.md.
|
|
16
|
+
*
|
|
17
|
+
* Design contract — the SAFETY rule that makes A safe:
|
|
18
|
+
* "unknown" (a gate's trigger field is absent) ⇒ treat as APPLIES for the
|
|
19
|
+
* UI (show the field) and as `unverifiable_conditional` for the verdict
|
|
20
|
+
* (warn, never assert). We NEVER hide or suppress a field on uncertainty —
|
|
21
|
+
* a mis-classified battery must never lose a legally-required field.
|
|
22
|
+
*
|
|
23
|
+
* Triggers:
|
|
24
|
+
* - batteryCategory — a real template field in battery.json (enum: LMT |
|
|
25
|
+
* EV | industrial_gt_2kwh | industrial_lte_2kwh | SLI
|
|
26
|
+
* | portable). Read from `passport.fields`.
|
|
27
|
+
* - hasBMS, rechargeable, externalStorageOnly, isStationaryBess — tri-state
|
|
28
|
+
* booleans on `passport.batteryProfile` (a separate
|
|
29
|
+
* block, NOT template fields — keeps the template's
|
|
30
|
+
* field count intact). Absent ⇒ the dependent gate
|
|
31
|
+
* resolves to "unknown" (show + warn), not a guess.
|
|
32
|
+
*
|
|
33
|
+
* Pure + IO-free → unit-tested in tests/compliance/battery-applicability.test.ts.
|
|
34
|
+
*/
|
|
35
|
+
import { resolveConditionProfile } from "./condition-profile.js";
|
|
36
|
+
import { IN_SCOPE_BATTERY_CATEGORIES } from "./battery-scope.js";
|
|
37
|
+
/** Battery categories that are *in scope* for a battery passport at all.
|
|
38
|
+
* Re-exported from the shared definition so this module's consumers keep
|
|
39
|
+
* importing it from here; the passport-level scope gate is rules.ts BAT-1. */
|
|
40
|
+
export { IN_SCOPE_BATTERY_CATEGORIES };
|
|
41
|
+
/** Field keys whose applicability is conditional, with a short reason +
|
|
42
|
+
* the deck article they map to. Everything NOT listed here is always
|
|
43
|
+
* "applies". Kept as data so the rules engine + editor can enumerate the
|
|
44
|
+
* conditional set and so a reader can audit each gate against the deck. */
|
|
45
|
+
/** Remediation for the gates that depend on `isStationaryBess`. */
|
|
46
|
+
const SBESS_FIX_HINT = "For an industrial (>2 kWh) battery, confirm whether it is a stationary battery energy storage system: Art. 14 state-of-health and Annex VII expected-lifetime data apply only to stationary storage systems, LMT and (for state of health) EV batteries.";
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a profile flag to a confirmed boolean. Only an `approved` flag
|
|
49
|
+
* counts — a `pending_review` (AI-suggested, unconfirmed) flag resolves to
|
|
50
|
+
* `undefined` ("unknown") so it can NEVER hard-hide a field. This is the
|
|
51
|
+
* safety contract: AI may suggest, but gating tightens only on confirmation.
|
|
52
|
+
*
|
|
53
|
+
* Accepts any object with `value: boolean` and `status: string` — works for
|
|
54
|
+
* both the legacy `BatteryProfileFlag` and the new `ConditionFlag`.
|
|
55
|
+
*/
|
|
56
|
+
function confirmed(flag) {
|
|
57
|
+
if (!flag)
|
|
58
|
+
return undefined;
|
|
59
|
+
return flag.status === "approved" ? flag.value : undefined;
|
|
60
|
+
}
|
|
61
|
+
function readTriggers(passport) {
|
|
62
|
+
const cat = passport.fields["batteryCategory"];
|
|
63
|
+
// Use the canonical accessor so conditionProfile (new) and batteryProfile
|
|
64
|
+
// (legacy) both work through the same path.
|
|
65
|
+
const profile = resolveConditionProfile(passport);
|
|
66
|
+
return {
|
|
67
|
+
category: cat && cat.value != null && cat.value !== "" ? String(cat.value) : undefined,
|
|
68
|
+
hasBMS: confirmed(profile["hasBMS"]),
|
|
69
|
+
rechargeable: confirmed(profile["rechargeable"]),
|
|
70
|
+
externalStorageOnly: confirmed(profile["externalStorageOnly"]),
|
|
71
|
+
isStationaryBess: confirmed(profile["isStationaryBess"]),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Is this battery an Art. 14(1) subject (state-of-health / expected-lifetime
|
|
76
|
+
* data applies)? Art. 14(1) covers EV, LMT, and stationary battery energy
|
|
77
|
+
* storage systems — NOT other industrial batteries.
|
|
78
|
+
*
|
|
79
|
+
* Returns:
|
|
80
|
+
* true — EV or LMT (deterministic from category alone)
|
|
81
|
+
* true/false — industrial_gt_2kwh resolved through isStationaryBess
|
|
82
|
+
* undefined — category absent (cannot determine) or
|
|
83
|
+
* industrial_gt_2kwh with isStationaryBess unconfirmed
|
|
84
|
+
* false — any other category (portable, SLI, industrial_lte_2kwh)
|
|
85
|
+
*/
|
|
86
|
+
function art14Subject(t) {
|
|
87
|
+
if (t.category === undefined)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (t.category === "EV" || t.category === "LMT")
|
|
90
|
+
return true;
|
|
91
|
+
if (t.category === "industrial_gt_2kwh")
|
|
92
|
+
return t.isStationaryBess;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Annex VII Part A + B applicability: applies for LMT; for industrial_gt_2kwh
|
|
97
|
+
* only when confirmed as a stationary BESS; not_applicable for EV and all
|
|
98
|
+
* other categories; unknown when category is absent or isStationaryBess unset.
|
|
99
|
+
*
|
|
100
|
+
* Used for: the Annex VII Part A cluster (remaining capacity, pack resistance,
|
|
101
|
+
* etc.) and the Annex VII Part B cluster (expected-lifetime: capacity/energy
|
|
102
|
+
* throughput, date of service entry).
|
|
103
|
+
*/
|
|
104
|
+
function sbessOrLmt(t) {
|
|
105
|
+
if (t.category === undefined)
|
|
106
|
+
return "unknown";
|
|
107
|
+
if (t.category === "LMT")
|
|
108
|
+
return "applies";
|
|
109
|
+
if (t.category === "industrial_gt_2kwh") {
|
|
110
|
+
if (t.isStationaryBess === true)
|
|
111
|
+
return "applies";
|
|
112
|
+
if (t.isStationaryBess === false)
|
|
113
|
+
return "not_applicable";
|
|
114
|
+
return "unknown";
|
|
115
|
+
}
|
|
116
|
+
// EV uses SOCE instead; portable / SLI / industrial_lte_2kwh are out of scope.
|
|
117
|
+
return "not_applicable";
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The conditional gates, one per applicability rule from the deck. Order
|
|
121
|
+
* doesn't matter — each governs a disjoint set of field keys.
|
|
122
|
+
*/
|
|
123
|
+
export const BATTERY_FIELD_GATES = [
|
|
124
|
+
{
|
|
125
|
+
// Deck 1(k): "capacity threshold for exhaustion (only for electric
|
|
126
|
+
// vehicle batteries)". Resolvable today — trigger is batteryCategory.
|
|
127
|
+
keys: ["capacityThresholdForExhaustion"],
|
|
128
|
+
article: "Annex XIII 1(k)",
|
|
129
|
+
reason: "Capacity threshold for exhaustion applies only to electric-vehicle (EV) batteries.",
|
|
130
|
+
decide: (t) => {
|
|
131
|
+
if (t.category === undefined)
|
|
132
|
+
return "unknown";
|
|
133
|
+
return t.category === "EV" ? "applies" : "not_applicable";
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
// Annex XIII 4(b) / Art. 14(1): state of health applies to stationary
|
|
138
|
+
// battery energy storage systems (SBESS), LMT, and EV batteries — and
|
|
139
|
+
// only when they have a battery management system. A plain industrial
|
|
140
|
+
// battery that is NOT a stationary ESS does NOT owe state-of-health data.
|
|
141
|
+
// art14Subject encodes the Art. 14(1) subject set; hasBMS is the second
|
|
142
|
+
// condition. Either being false → not_applicable; either absent → unknown.
|
|
143
|
+
keys: ["stateOfHealth"],
|
|
144
|
+
article: "Annex XIII 4(b) / Art. 14",
|
|
145
|
+
fixHint: SBESS_FIX_HINT,
|
|
146
|
+
reason: "State of health (Art. 14) applies only to stationary battery energy storage systems, LMT, and EV batteries that have a battery management system (BMS). A plain industrial (>2 kWh) battery that is not a stationary storage system does not owe this data.",
|
|
147
|
+
decide: (t) => {
|
|
148
|
+
const a14 = art14Subject(t);
|
|
149
|
+
// Either axis definitively false → field does not apply.
|
|
150
|
+
if (a14 === false || t.hasBMS === false)
|
|
151
|
+
return "not_applicable";
|
|
152
|
+
// Either axis unknown (category absent, or SBESS unconfirmed) → cannot determine.
|
|
153
|
+
if (a14 === undefined || t.hasBMS === undefined)
|
|
154
|
+
return "unknown";
|
|
155
|
+
// Both confirmed true → applies.
|
|
156
|
+
return "applies";
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
// Deck (c): carbon footprint cross-references Art. 7, which is
|
|
161
|
+
// rechargeable-only ("If the battery is non-rechargeable, (c) remains
|
|
162
|
+
// empty"). rechargeable absent ⇒ unknown.
|
|
163
|
+
keys: [
|
|
164
|
+
"carbonFootprintTotal",
|
|
165
|
+
"carbonFootprintPerformanceClass",
|
|
166
|
+
"carbonFootprintLabel",
|
|
167
|
+
"carbonFootprintStudyUrl",
|
|
168
|
+
"cfRawMaterialAcquisition",
|
|
169
|
+
"cfMainProductProduction",
|
|
170
|
+
"cfDistribution",
|
|
171
|
+
"cfEndOfLifeRecycling",
|
|
172
|
+
],
|
|
173
|
+
article: "Annex XIII 1(c) / Art. 7",
|
|
174
|
+
reason: "Carbon-footprint declaration applies only to rechargeable batteries.",
|
|
175
|
+
decide: (t) => {
|
|
176
|
+
if (t.rechargeable === undefined)
|
|
177
|
+
return "unknown";
|
|
178
|
+
return t.rechargeable ? "applies" : "not_applicable";
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
// Deck (e): recycled content cross-references Art. 8 ("If the battery
|
|
183
|
+
// has external storage only, (e) remains empty"). externalStorageOnly
|
|
184
|
+
// absent ⇒ unknown.
|
|
185
|
+
keys: [
|
|
186
|
+
"recycledContentCobalt",
|
|
187
|
+
"recycledContentLead",
|
|
188
|
+
"recycledContentLithium",
|
|
189
|
+
"recycledContentNickel",
|
|
190
|
+
"recycledContentDocumentation",
|
|
191
|
+
// Pre/post-consumer split-shares (Annex XIII 1(e)) carry the same gate
|
|
192
|
+
// as the combined figures above — external-storage-only batteries are exempt.
|
|
193
|
+
"preConsumerRecycledNickelShare",
|
|
194
|
+
"preConsumerRecycledCobaltShare",
|
|
195
|
+
"preConsumerRecycledLithiumShare",
|
|
196
|
+
"postConsumerRecycledNickelShare",
|
|
197
|
+
"postConsumerRecycledCobaltShare",
|
|
198
|
+
"postConsumerRecycledLithiumShare",
|
|
199
|
+
],
|
|
200
|
+
article: "Annex XIII 1(e) / Art. 8",
|
|
201
|
+
reason: "Recycled-content information does not apply to industrial batteries with exclusively external storage.",
|
|
202
|
+
decide: (t) => {
|
|
203
|
+
// Art. 8(1) exempts only "industrial batteries … except those with
|
|
204
|
+
// exclusively external storage"; EV and LMT batteries owe it regardless.
|
|
205
|
+
if (t.category !== undefined && t.category !== "industrial_gt_2kwh")
|
|
206
|
+
return "applies";
|
|
207
|
+
if (t.externalStorageOnly === undefined)
|
|
208
|
+
return "unknown";
|
|
209
|
+
return t.externalStorageOnly ? "not_applicable" : "applies";
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
// Annex XIII 4(b) / Art. 14: state of certified energy (SOCE) applies
|
|
214
|
+
// only to electric-vehicle (EV) batteries. Resolvable from batteryCategory.
|
|
215
|
+
keys: ["stateOfCertifiedEnergy"],
|
|
216
|
+
article: "Annex XIII 4(b) / Art. 14",
|
|
217
|
+
reason: "State of certified energy (SOCE) applies only to electric-vehicle (EV) batteries.",
|
|
218
|
+
decide: (t) => {
|
|
219
|
+
if (t.category === undefined)
|
|
220
|
+
return "unknown";
|
|
221
|
+
return t.category === "EV" ? "applies" : "not_applicable";
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
// Annex XIII 4(b) / Art. 14 / Annex VII Part A: remaining capacity /
|
|
226
|
+
// power capability, remaining round-trip efficiency (DP 64), self-discharge
|
|
227
|
+
// evolution, pack ohmic resistance, and initial self-discharge rate apply
|
|
228
|
+
// only to SBESS and LMT batteries — NOT EV (which has SOCE instead), and
|
|
229
|
+
// NOT plain industrial batteries that are not stationary storage systems.
|
|
230
|
+
// Art. 14(1) subjects: SBESS + LMT + EV. Annex VII Part A applies to
|
|
231
|
+
// SBESS + LMT. sbessOrLmt() resolves this correctly.
|
|
232
|
+
// NOTE: roundTripEfficiencyFade (DP 58) is "if applicable" in ALL THREE
|
|
233
|
+
// guidance columns and therefore is NOT in this gate — it applies to EV too.
|
|
234
|
+
keys: [
|
|
235
|
+
"remainingCapacity",
|
|
236
|
+
"remainingPowerCapability",
|
|
237
|
+
"remainingRoundTripEfficiency",
|
|
238
|
+
"evolutionOfSelfDischargeRate",
|
|
239
|
+
"currentInternalResistancePack",
|
|
240
|
+
// initialSelfDischargeRate cites Annex VII Part A(4) — same applicability
|
|
241
|
+
// as the rest of the Part A cluster.
|
|
242
|
+
"initialSelfDischargeRate",
|
|
243
|
+
],
|
|
244
|
+
article: "Annex XIII 4(b) / Art. 14 / Annex VII Part A",
|
|
245
|
+
fixHint: SBESS_FIX_HINT,
|
|
246
|
+
reason: "Remaining capacity, remaining power capability, remaining round-trip efficiency, self-discharge evolution, pack ohmic resistance, and initial self-discharge rate (Annex VII Part A) apply only to LMT batteries and stationary battery energy storage systems, not EV or non-stationary industrial batteries.",
|
|
247
|
+
decide: sbessOrLmt,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
// Annex XIII 4(b) / Art. 14 / Annex VII Part B: expected-lifetime
|
|
251
|
+
// parameters — capacity throughput, energy throughput, and date of first
|
|
252
|
+
// service entry — apply only to SBESS and LMT batteries (Annex VII Part B).
|
|
253
|
+
// EV carries SOCE + the capacity-threshold gate instead; a plain industrial
|
|
254
|
+
// battery that is not a stationary storage system does not owe this data.
|
|
255
|
+
// The template already marks these notApplicable for EV via requiredBy;
|
|
256
|
+
// the gate enforces that at the applicability-warning layer too.
|
|
257
|
+
keys: [
|
|
258
|
+
"capacityThroughput",
|
|
259
|
+
"energyThroughput",
|
|
260
|
+
"dateOfServiceEntry",
|
|
261
|
+
],
|
|
262
|
+
article: "Annex XIII 4(b) / Art. 14 / Annex VII Part B",
|
|
263
|
+
fixHint: SBESS_FIX_HINT,
|
|
264
|
+
reason: "Expected-lifetime parameters (Annex VII Part B) apply only to LMT batteries and stationary battery energy storage systems.",
|
|
265
|
+
decide: sbessOrLmt,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
// Phase 1 — rechargeable gate: 12 performance fields that derive from the
|
|
269
|
+
// Art. 10(1) performance document, which only exists for rechargeable batteries.
|
|
270
|
+
//
|
|
271
|
+
// Category awareness: EV and LMT batteries are inherently rechargeable by
|
|
272
|
+
// definition — no flag check is needed. The gate always returns "applies" for
|
|
273
|
+
// those categories, matching the "required" entries in the template's requiredBy
|
|
274
|
+
// map. For industrial_gt_2kwh the flag is decisive: an approved rechargeable=true
|
|
275
|
+
// promotes the conditional template entry to a hard publish block; rechargeable=false
|
|
276
|
+
// means the Art. 10(1) duty does not arise.
|
|
277
|
+
//
|
|
278
|
+
// Note: dynamicEnergyRoundTripEfficiency (DP 57) is "if applicable" for ALL three
|
|
279
|
+
// categories and is NOT in this gate — it carries no requiredBy map at all.
|
|
280
|
+
//
|
|
281
|
+
// Legal basis:
|
|
282
|
+
// Annex XIII 1(j) — expected lifetime, cycle data, performance-fade metrics
|
|
283
|
+
// Annex XIII 1(n) — initial / 50%-cycle-life round-trip efficiency
|
|
284
|
+
// Annex XIII 4(a)(i/ii/iv) — dynamic rated capacity, power, internal resistance
|
|
285
|
+
// Art. 10(1) — performance document for rechargeable industrial >2 kWh,
|
|
286
|
+
// EV, and LMT batteries
|
|
287
|
+
keys: [
|
|
288
|
+
// Static performance (Annex XIII 1(j)/(n) / Art. 10(1))
|
|
289
|
+
"initialEnergyRoundTripEfficiency",
|
|
290
|
+
"roundTripEfficiencyAtHalfCycleLife",
|
|
291
|
+
// Expected lifetime (Annex XIII 4(c) / Art. 10(1))
|
|
292
|
+
"expectedLifetimeCycles",
|
|
293
|
+
"expectedLifetimeYears",
|
|
294
|
+
"expectedLifetimeReferenceConditions",
|
|
295
|
+
// Performance test parameters (Art. 10(1))
|
|
296
|
+
"cRateRelevantCycleLifeTest",
|
|
297
|
+
// State-of-health performance metrics (Art. 10(1) / Annex XIII 4(b) partial)
|
|
298
|
+
"internalResistanceIncrease",
|
|
299
|
+
"capacityFade",
|
|
300
|
+
"powerFade",
|
|
301
|
+
// Dynamic performance data (Annex XIII 4(a)(i/ii/iv)) — updated on status change
|
|
302
|
+
// Note: 4(a)(iii) = dynamicEnergyRoundTripEfficiency is "if applicable" → not gated
|
|
303
|
+
"dynamicRatedCapacity",
|
|
304
|
+
"dynamicPowerCapability",
|
|
305
|
+
"dynamicInternalResistance",
|
|
306
|
+
],
|
|
307
|
+
article: "Annex XIII 1(j)/(n), 4(a)(i/ii/iv), Art. 10(1)",
|
|
308
|
+
reason: "These performance fields come from the Art. 10(1) performance document (rechargeable industrial >2 kWh, EV, LMT). EV and LMT are inherently rechargeable; the flag governs industrial_gt_2kwh only.",
|
|
309
|
+
decide: (t) => {
|
|
310
|
+
// EV and LMT are rechargeable by definition — the flag is irrelevant for them.
|
|
311
|
+
if (t.category === "EV" || t.category === "LMT")
|
|
312
|
+
return "applies";
|
|
313
|
+
// industrial_gt_2kwh: the rechargeable flag is decisive.
|
|
314
|
+
if (t.rechargeable === undefined)
|
|
315
|
+
return "unknown";
|
|
316
|
+
return t.rechargeable ? "applies" : "not_applicable";
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
/** Pre-computed map from field key → the gate governing it. */
|
|
321
|
+
const KEY_TO_GATE = new Map();
|
|
322
|
+
for (const gate of BATTERY_FIELD_GATES) {
|
|
323
|
+
for (const k of gate.keys)
|
|
324
|
+
KEY_TO_GATE.set(k, gate);
|
|
325
|
+
}
|
|
326
|
+
/** Every field key under any gate (the conditional set). */
|
|
327
|
+
export const GATED_FIELD_KEYS = new Set(KEY_TO_GATE.keys());
|
|
328
|
+
/**
|
|
329
|
+
* Applicability for a single battery field key. Ungated keys are always
|
|
330
|
+
* "applies". This is the granular accessor the editor uses per-field.
|
|
331
|
+
*/
|
|
332
|
+
export function fieldApplicability(passport, key) {
|
|
333
|
+
const gate = KEY_TO_GATE.get(key);
|
|
334
|
+
if (!gate)
|
|
335
|
+
return "applies";
|
|
336
|
+
return gate.decide(readTriggers(passport));
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Full map for the gated fields only (callers default everything else to
|
|
340
|
+
* "applies"). The verdict engine iterates this to emit findings; the
|
|
341
|
+
* editor reads it once per render and looks up per field.
|
|
342
|
+
*
|
|
343
|
+
* Returns an empty map for non-battery passports — `category` here is the
|
|
344
|
+
* template category, NOT batteryCategory; callers pass it so this is a
|
|
345
|
+
* no-op outside battery and the predicate stays battery-scoped.
|
|
346
|
+
*/
|
|
347
|
+
/** @deprecated Use `categoryFieldApplicability` instead. Kept for backward compat. */
|
|
348
|
+
export function batteryFieldApplicability(passport, category) {
|
|
349
|
+
return categoryFieldApplicability(passport, category);
|
|
350
|
+
}
|
|
351
|
+
/** Look up the gate metadata (article + reason) for a gated field key. */
|
|
352
|
+
export function gateForKey(key) {
|
|
353
|
+
return KEY_TO_GATE.get(key);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* CATEGORY_FIELD_GATES — generalised gate registry, keyed by template category.
|
|
357
|
+
*
|
|
358
|
+
* Phase 1: battery only. Other categories gain entries when their conditional
|
|
359
|
+
* rules are encoded. Battery gates are unchanged from BATTERY_FIELD_GATES; the
|
|
360
|
+
* two names refer to the same array.
|
|
361
|
+
*
|
|
362
|
+
* Callers that need to enumerate gates for a category use this registry.
|
|
363
|
+
* The battery-specific `batteryFieldApplicability` is now a thin wrapper.
|
|
364
|
+
*/
|
|
365
|
+
export const CATEGORY_FIELD_GATES = {
|
|
366
|
+
battery: BATTERY_FIELD_GATES,
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* Compute the applicability map for all gated fields of any category.
|
|
370
|
+
* Returns {} for categories with no gates (safe to iterate).
|
|
371
|
+
*
|
|
372
|
+
* Replaces the battery-only early return in `batteryFieldApplicability` with a
|
|
373
|
+
* generic path: every category that has gates in CATEGORY_FIELD_GATES is handled,
|
|
374
|
+
* everything else is a no-op. `batteryFieldApplicability` is kept as a
|
|
375
|
+
* backward-compat alias.
|
|
376
|
+
*/
|
|
377
|
+
export function categoryFieldApplicability(passport, category) {
|
|
378
|
+
const gates = CATEGORY_FIELD_GATES[category];
|
|
379
|
+
if (!gates || gates.length === 0)
|
|
380
|
+
return {};
|
|
381
|
+
const triggers = readTriggers(passport);
|
|
382
|
+
const out = {};
|
|
383
|
+
for (const gate of gates) {
|
|
384
|
+
const verdict = gate.decide(triggers);
|
|
385
|
+
for (const k of gate.keys)
|
|
386
|
+
out[k] = verdict;
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
//# sourceMappingURL=battery-applicability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"battery-applicability.js","sourceRoot":"","sources":["../../src/vendor/battery-applicability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAGH,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AAIjE;;+EAE+E;AAC/E,OAAO,EAAE,2BAA2B,EAAE,CAAC;AAEvC;;;4EAG4E;AAC5E,mEAAmE;AACnE,MAAM,cAAc,GAClB,0PAA0P,CAAC;AAuB7P;;;;;;;;GAQG;AACH,SAAS,SAAS,CAAC,IAAoD;IACrE,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7D,CAAC;AAED,SAAS,YAAY,CAAC,QAAkB;IACtC,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;IAC/C,0EAA0E;IAC1E,4CAA4C;IAC5C,MAAM,OAAO,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAClD,OAAO;QACL,QAAQ,EAAE,GAAG,IAAI,GAAG,CAAC,KAAK,IAAI,IAAI,IAAI,GAAG,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS;QACtF,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACpC,YAAY,EAAE,SAAS,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QAChD,mBAAmB,EAAE,SAAS,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;QAC9D,gBAAgB,EAAE,SAAS,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;KACzD,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,YAAY,CAAC,CAAW;IAC/B,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC/C,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAC7D,IAAI,CAAC,CAAC,QAAQ,KAAK,oBAAoB;QAAE,OAAO,CAAC,CAAC,gBAAgB,CAAC;IACnE,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,UAAU,CAAC,CAAW;IAC7B,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC/C,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK;QAAE,OAAO,SAAS,CAAC;IAC3C,IAAI,CAAC,CAAC,QAAQ,KAAK,oBAAoB,EAAE,CAAC;QACxC,IAAI,CAAC,CAAC,gBAAgB,KAAK,IAAI;YAAE,OAAO,SAAS,CAAC;QAClD,IAAI,CAAC,CAAC,gBAAgB,KAAK,KAAK;YAAE,OAAO,gBAAgB,CAAC;QAC1D,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,+EAA+E;IAC/E,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAgB;IAC9C;QACE,mEAAmE;QACnE,sEAAsE;QACtE,IAAI,EAAE,CAAC,gCAAgC,CAAC;QACxC,OAAO,EAAE,iBAAiB;QAC1B,MAAM,EAAE,oFAAoF;QAC5F,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;YACZ,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YAC/C,OAAO,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC;QAC5D,CAAC;KACF;IACD;QACE,sEAAsE;QACtE,sEAAsE;QACtE,sEAAsE;QACtE,0EAA0E;QAC1E,wEAAwE;QACxE,2EAA2E;QAC3E,IAAI,EAAE,CAAC,eAAe,CAAC;QACvB,OAAO,EAAE,2BAA2B;QACpC,OAAO,EAAE,cAAc;QACvB,MAAM,EAAE,6PAA6P;QACrQ,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;YACZ,MAAM,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;YAC5B,yDAAyD;YACzD,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,MAAM,KAAK,KAAK;gBAAE,OAAO,gBAAgB,CAAC;YACjE,kFAAkF;YAClF,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YAClE,iCAAiC;YACjC,OAAO,SAAS,CAAC;QACnB,CAAC;KACF;IACD;QACE,+DAA+D;QAC/D,sEAAsE;QACtE,0CAA0C;QAC1C,IAAI,EAAE;YACJ,sBAAsB;YACtB,iCAAiC;YACjC,sBAAsB;YACtB,yBAAyB;YACzB,0BAA0B;YAC1B,yBAAyB;YACzB,gBAAgB;YAChB,sBAAsB;SACvB;QACD,OAAO,EAAE,0BAA0B;QACnC,MAAM,EAAE,sEAAsE;QAC9E,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;YACZ,IAAI,CAAC,CAAC,YAAY,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YACnD,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC;QACvD,CAAC;KACF;IACD;QACE,sEAAsE;QACtE,sEAAsE;QACtE,oBAAoB;QACpB,IAAI,EAAE;YACJ,uBAAuB;YACvB,qBAAqB;YACrB,wBAAwB;YACxB,uBAAuB;YACvB,8BAA8B;YAC9B,uEAAuE;YACvE,8EAA8E;YAC9E,gCAAgC;YAChC,gCAAgC;YAChC,iCAAiC;YACjC,iCAAiC;YACjC,iCAAiC;YACjC,kCAAkC;SACnC;QACD,OAAO,EAAE,0BAA0B;QACnC,MAAM,EAAE,wGAAwG;QAChH,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;YACZ,mEAAmE;YACnE,yEAAyE;YACzE,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,QAAQ,KAAK,oBAAoB;gBAAE,OAAO,SAAS,CAAC;YACtF,IAAI,CAAC,CAAC,mBAAmB,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YAC1D,OAAO,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9D,CAAC;KACF;IACD;QACE,sEAAsE;QACtE,4EAA4E;QAC5E,IAAI,EAAE,CAAC,wBAAwB,CAAC;QAChC,OAAO,EAAE,2BAA2B;QACpC,MAAM,EAAE,mFAAmF;QAC3F,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;YACZ,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YAC/C,OAAO,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC;QAC5D,CAAC;KACF;IACD;QACE,qEAAqE;QACrE,4EAA4E;QAC5E,0EAA0E;QAC1E,yEAAyE;QACzE,0EAA0E;QAC1E,qEAAqE;QACrE,qDAAqD;QACrD,wEAAwE;QACxE,6EAA6E;QAC7E,IAAI,EAAE;YACJ,mBAAmB;YACnB,0BAA0B;YAC1B,8BAA8B;YAC9B,8BAA8B;YAC9B,+BAA+B;YAC/B,0EAA0E;YAC1E,qCAAqC;YACrC,0BAA0B;SAC3B;QACD,OAAO,EAAE,8CAA8C;QACvD,OAAO,EAAE,cAAc;QACvB,MAAM,EAAE,gTAAgT;QACxT,MAAM,EAAE,UAAU;KACnB;IACD;QACE,kEAAkE;QAClE,yEAAyE;QACzE,4EAA4E;QAC5E,4EAA4E;QAC5E,0EAA0E;QAC1E,wEAAwE;QACxE,iEAAiE;QACjE,IAAI,EAAE;YACJ,oBAAoB;YACpB,kBAAkB;YAClB,oBAAoB;SACrB;QACD,OAAO,EAAE,8CAA8C;QACvD,OAAO,EAAE,cAAc;QACvB,MAAM,EAAE,4HAA4H;QACpI,MAAM,EAAE,UAAU;KACnB;IACD;QACE,0EAA0E;QAC1E,iFAAiF;QACjF,EAAE;QACF,0EAA0E;QAC1E,8EAA8E;QAC9E,iFAAiF;QACjF,kFAAkF;QAClF,sFAAsF;QACtF,4CAA4C;QAC5C,EAAE;QACF,kFAAkF;QAClF,4EAA4E;QAC5E,EAAE;QACF,eAAe;QACf,+EAA+E;QAC/E,sEAAsE;QACtE,kFAAkF;QAClF,gFAAgF;QAChF,6CAA6C;QAC7C,IAAI,EAAE;YACJ,wDAAwD;YACxD,kCAAkC;YAClC,oCAAoC;YACpC,mDAAmD;YACnD,wBAAwB;YACxB,uBAAuB;YACvB,qCAAqC;YACrC,2CAA2C;YAC3C,4BAA4B;YAC5B,6EAA6E;YAC7E,4BAA4B;YAC5B,cAAc;YACd,WAAW;YACX,iFAAiF;YACjF,oFAAoF;YACpF,sBAAsB;YACtB,wBAAwB;YACxB,2BAA2B;SAC5B;QACD,OAAO,EAAE,gDAAgD;QACzD,MAAM,EAAE,qMAAqM;QAC7M,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;YACZ,+EAA+E;YAC/E,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK;gBAAE,OAAO,SAAS,CAAC;YAClE,yDAAyD;YACzD,IAAI,CAAC,CAAC,YAAY,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YACnD,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC;QACvD,CAAC;KACF;CACF,CAAC;AAEF,+DAA+D;AAC/D,MAAM,WAAW,GAAG,IAAI,GAAG,EAAqB,CAAC;AACjD,KAAK,MAAM,IAAI,IAAI,mBAAmB,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI;QAAE,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AACtD,CAAC;AAED,4DAA4D;AAC5D,MAAM,CAAC,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;AAEjF;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAkB,EAAE,GAAW;IAChE,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;;GAQG;AACH,sFAAsF;AACtF,MAAM,UAAU,yBAAyB,CACvC,QAAkB,EAClB,QAAgB;IAEhB,OAAO,0BAA0B,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACxD,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,OAAO,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAgC;IAC/D,OAAO,EAAE,mBAAmB;CAC7B,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,UAAU,0BAA0B,CACxC,QAAkB,EAClB,QAAgB;IAEhB,MAAM,KAAK,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5C,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,GAAG,GAAkC,EAAE,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACtC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IAC9C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generalised condition-profile accessor.
|
|
3
|
+
*
|
|
4
|
+
* `conditionProfile` (the new field) wins when present; falls back to the
|
|
5
|
+
* legacy `batteryProfile` block, adapting it to the `ConditionFlag` shape
|
|
6
|
+
* (audit defaults to []). This mirrors the `gs1` → `identifier` precedence
|
|
7
|
+
* pattern: old data stays readable without a migration, and new writes land
|
|
8
|
+
* in the canonical location.
|
|
9
|
+
*
|
|
10
|
+
* **Use `resolveConditionProfile` everywhere** instead of reading
|
|
11
|
+
* `passport.batteryProfile` directly — this is the single source of truth
|
|
12
|
+
* for "what condition flags has a reviewer set?".
|
|
13
|
+
*
|
|
14
|
+
* Pure + IO-free → unit-tested in tests/compliance/condition-profile.test.ts.
|
|
15
|
+
*/
|
|
16
|
+
import type { Passport, ConditionProfile } from "@tracepass/dpp-types";
|
|
17
|
+
/**
|
|
18
|
+
* Return the effective condition profile for a passport, regardless of which
|
|
19
|
+
* storage field holds the data.
|
|
20
|
+
*
|
|
21
|
+
* - If `passport.conditionProfile` is present (even as `{}`), return it as-is.
|
|
22
|
+
* An empty conditionProfile means "all flags cleared" — it must NOT fall back
|
|
23
|
+
* to batteryProfile, or clearing the last flag would resurrect legacy data.
|
|
24
|
+
* - Otherwise, adapt `passport.batteryProfile` (the legacy shape, which lacks
|
|
25
|
+
* per-flag audit arrays) to the `ConditionProfile` shape with `audit: []`.
|
|
26
|
+
* - If neither is set, return an empty object.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveConditionProfile(passport: Pick<Passport, "conditionProfile" | "batteryProfile">): ConditionProfile;
|
|
29
|
+
//# sourceMappingURL=condition-profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"condition-profile.d.ts","sourceRoot":"","sources":["../../src/vendor/condition-profile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEvE;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,gBAAgB,CAAC,GAAG,gBAAgB,CAkBzH"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generalised condition-profile accessor.
|
|
3
|
+
*
|
|
4
|
+
* `conditionProfile` (the new field) wins when present; falls back to the
|
|
5
|
+
* legacy `batteryProfile` block, adapting it to the `ConditionFlag` shape
|
|
6
|
+
* (audit defaults to []). This mirrors the `gs1` → `identifier` precedence
|
|
7
|
+
* pattern: old data stays readable without a migration, and new writes land
|
|
8
|
+
* in the canonical location.
|
|
9
|
+
*
|
|
10
|
+
* **Use `resolveConditionProfile` everywhere** instead of reading
|
|
11
|
+
* `passport.batteryProfile` directly — this is the single source of truth
|
|
12
|
+
* for "what condition flags has a reviewer set?".
|
|
13
|
+
*
|
|
14
|
+
* Pure + IO-free → unit-tested in tests/compliance/condition-profile.test.ts.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Return the effective condition profile for a passport, regardless of which
|
|
18
|
+
* storage field holds the data.
|
|
19
|
+
*
|
|
20
|
+
* - If `passport.conditionProfile` is present (even as `{}`), return it as-is.
|
|
21
|
+
* An empty conditionProfile means "all flags cleared" — it must NOT fall back
|
|
22
|
+
* to batteryProfile, or clearing the last flag would resurrect legacy data.
|
|
23
|
+
* - Otherwise, adapt `passport.batteryProfile` (the legacy shape, which lacks
|
|
24
|
+
* per-flag audit arrays) to the `ConditionProfile` shape with `audit: []`.
|
|
25
|
+
* - If neither is set, return an empty object.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveConditionProfile(passport) {
|
|
28
|
+
if (passport.conditionProfile !== undefined) {
|
|
29
|
+
return passport.conditionProfile;
|
|
30
|
+
}
|
|
31
|
+
const bp = passport.batteryProfile;
|
|
32
|
+
if (!bp)
|
|
33
|
+
return {};
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [key, flag] of Object.entries(bp)) {
|
|
36
|
+
if (flag != null) {
|
|
37
|
+
out[key] = {
|
|
38
|
+
value: flag.value,
|
|
39
|
+
status: flag.status,
|
|
40
|
+
source: flag.source,
|
|
41
|
+
audit: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=condition-profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"condition-profile.js","sourceRoot":"","sources":["../../src/vendor/condition-profile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;;;;;;;;GAUG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAA+D;IACrG,IAAI,QAAQ,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;QAC5C,OAAO,QAAQ,CAAC,gBAAgB,CAAC;IACnC,CAAC;IACD,MAAM,EAAE,GAAG,QAAQ,CAAC,cAAc,CAAC;IACnC,IAAI,CAAC,EAAE;QAAE,OAAO,EAAE,CAAC;IACnB,MAAM,GAAG,GAAqB,EAAE,CAAC;IACjC,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,CAA2C,EAAE,CAAC;QACvF,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;YACjB,GAAG,CAAC,GAAG,CAAC,GAAG;gBACT,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,KAAK,EAAE,EAAE;aACV,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -19,24 +19,35 @@
|
|
|
19
19
|
* and commit `mirror-manifest.json` with the code.
|
|
20
20
|
*/
|
|
21
21
|
import type { Passport, Template, TemplateField } from "@tracepass/dpp-types";
|
|
22
|
+
/** Applicability value returned by the condition-gate engine. */
|
|
23
|
+
export type GateApplicability = "applies" | "not_applicable" | "unknown";
|
|
22
24
|
export interface PublishCheck {
|
|
23
25
|
ready: boolean;
|
|
24
26
|
/** Field keys that have no value set. */
|
|
25
27
|
missingFields: string[];
|
|
26
28
|
/** Field keys with a value but status != approved. */
|
|
27
29
|
unapprovedFields: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Field keys with `requiredBy[category] === "conditional"` whose gate
|
|
32
|
+
* resolved `"applies"` (from an APPROVED flag) and the field is empty.
|
|
33
|
+
* This is a HARD publish block — stricter than a soft gap, because the
|
|
34
|
+
* owner has confirmed the condition holds.
|
|
35
|
+
*/
|
|
36
|
+
conditionalMissingFields: string[];
|
|
28
37
|
/** Short human reason suitable for a tooltip or toast — undefined when ready. */
|
|
29
38
|
reason?: string;
|
|
30
39
|
/**
|
|
31
40
|
* Distinguishes WHY the passport isn't ready:
|
|
32
|
-
* - "hard"
|
|
33
|
-
*
|
|
34
|
-
* - "gap"
|
|
35
|
-
*
|
|
36
|
-
*
|
|
41
|
+
* - "hard" → structural blocker that can't be bypassed
|
|
42
|
+
* (already-published, wrong status, template missing).
|
|
43
|
+
* - "gap" → required fields are missing or unapproved. The UI
|
|
44
|
+
* can offer to publish anyway with an explicit "I
|
|
45
|
+
* understand the risk" acknowledgement.
|
|
46
|
+
* - "conditional" → a condition-flag gate confirmed a duty applies and
|
|
47
|
+
* the field is empty. Hard block, same as "hard".
|
|
37
48
|
* - `undefined` when ready === true.
|
|
38
49
|
*/
|
|
39
|
-
blockerType?: "hard" | "gap";
|
|
50
|
+
blockerType?: "hard" | "gap" | "conditional";
|
|
40
51
|
}
|
|
41
52
|
/**
|
|
42
53
|
* Resolves whether a template field is effectively required for a specific
|
|
@@ -100,6 +111,31 @@ export declare function effectiveRequiredFor(templateCategory: string, tf: Pick<
|
|
|
100
111
|
* absence of information is never grounds for asserting non-applicability.
|
|
101
112
|
*/
|
|
102
113
|
export declare function notApplicableForCategory(tf: Pick<TemplateField, "validation">, category: string | undefined): boolean;
|
|
114
|
+
/** The field-level findings returned by `evaluateFieldRequirements`. */
|
|
115
|
+
export interface FieldRequirements {
|
|
116
|
+
missingFields: string[];
|
|
117
|
+
unapprovedFields: string[];
|
|
118
|
+
conditionalMissingFields: string[];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Evaluate field-level requirements for a passport against its template,
|
|
122
|
+
* status-independently. Called by both `checkPublishReady` (which adds
|
|
123
|
+
* status early-returns) and `evaluateCompliance` (which must run even on
|
|
124
|
+
* published passports so live DPPs are re-evaluated after a template or
|
|
125
|
+
* flag change).
|
|
126
|
+
*
|
|
127
|
+
* Returns three buckets:
|
|
128
|
+
* - `missingFields` — statically required, no value set
|
|
129
|
+
* - `unapprovedFields` — statically required, value set but not approved
|
|
130
|
+
* - `conditionalMissingFields` — gate-confirmed conditional duty, absent
|
|
131
|
+
* OR present-but-unapproved (hard block)
|
|
132
|
+
*
|
|
133
|
+
* `applicabilityMap` is pre-computed by the caller via
|
|
134
|
+
* `categoryFieldApplicability(passport, template.category)`. Pass `{}` for
|
|
135
|
+
* non-battery categories — the gate branch is then a no-op. Keeping it
|
|
136
|
+
* injected preserves the IO-free/pure contract of this function.
|
|
137
|
+
*/
|
|
138
|
+
export declare function evaluateFieldRequirements(passport: Passport, template: Template, applicabilityMap?: Record<string, GateApplicability>): FieldRequirements;
|
|
103
139
|
/**
|
|
104
140
|
* Returns whether `passport` satisfies every publish precondition given
|
|
105
141
|
* its `template`. A passport is publish-ready iff:
|
|
@@ -107,9 +143,19 @@ export declare function notApplicableForCategory(tf: Pick<TemplateField, "valida
|
|
|
107
143
|
* states return ready:false with a specific reason),
|
|
108
144
|
* - every template field whose effective required flag (resolved via
|
|
109
145
|
* `effectiveRequired` — which consults `requiredBy` when present) is
|
|
110
|
-
* true has a non-empty value AND status === "approved"
|
|
146
|
+
* true has a non-empty value AND status === "approved",
|
|
147
|
+
* - every `requiredBy[category] === "conditional"` field whose gate
|
|
148
|
+
* resolves `"applies"` (from an APPROVED condition flag) also has a
|
|
149
|
+
* non-empty value AND status === "approved" (hard block, same severity
|
|
150
|
+
* as a structural blocker).
|
|
111
151
|
*
|
|
112
152
|
* `undefined` template means we can't evaluate — treat as not-ready.
|
|
153
|
+
*
|
|
154
|
+
* @param applicabilityMap Pre-computed gate-applicability map (field key →
|
|
155
|
+
* "applies" | "not_applicable" | "unknown"). Computed by the caller using
|
|
156
|
+
* `categoryFieldApplicability(passport, template.category)`. Pass `{}` or
|
|
157
|
+
* omit for non-battery passports (no change in behaviour). Keeping it
|
|
158
|
+
* injected preserves the IO-free/pure contract of this function.
|
|
113
159
|
*/
|
|
114
|
-
export declare function checkPublishReady(passport: Passport, template: Template | undefined): PublishCheck;
|
|
160
|
+
export declare function checkPublishReady(passport: Passport, template: Template | undefined, applicabilityMap?: Record<string, GateApplicability>): PublishCheck;
|
|
115
161
|
//# sourceMappingURL=publish-gate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-gate.d.ts","sourceRoot":"","sources":["../../src/vendor/publish-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI9E,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,yCAAyC;IACzC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB
|
|
1
|
+
{"version":3,"file":"publish-gate.d.ts","sourceRoot":"","sources":["../../src/vendor/publish-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI9E,iEAAiE;AACjE,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,gBAAgB,GAAG,SAAS,CAAC;AAEzE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,yCAAyC;IACzC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;;OAKG;IACH,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,aAAa,CAAC;CAC9C;AAQD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,EACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAUT;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,2BAA2B,CACzC,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,EACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAKT;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,gBAAgB,EAAE,MAAM,EACxB,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,EACrC,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAIT;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,EACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAIT;AAED,wEAAwE;AACxE,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,wBAAwB,EAAE,MAAM,EAAE,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,QAAQ,EAClB,gBAAgB,GAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAM,GACvD,iBAAiB,CA2DnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,gBAAgB,GAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAM,GACvD,YAAY,CAoEd"}
|
|
@@ -110,6 +110,80 @@ export function notApplicableForCategory(tf, category) {
|
|
|
110
110
|
return false;
|
|
111
111
|
return requiredBy[category] === "notApplicable";
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Evaluate field-level requirements for a passport against its template,
|
|
115
|
+
* status-independently. Called by both `checkPublishReady` (which adds
|
|
116
|
+
* status early-returns) and `evaluateCompliance` (which must run even on
|
|
117
|
+
* published passports so live DPPs are re-evaluated after a template or
|
|
118
|
+
* flag change).
|
|
119
|
+
*
|
|
120
|
+
* Returns three buckets:
|
|
121
|
+
* - `missingFields` — statically required, no value set
|
|
122
|
+
* - `unapprovedFields` — statically required, value set but not approved
|
|
123
|
+
* - `conditionalMissingFields` — gate-confirmed conditional duty, absent
|
|
124
|
+
* OR present-but-unapproved (hard block)
|
|
125
|
+
*
|
|
126
|
+
* `applicabilityMap` is pre-computed by the caller via
|
|
127
|
+
* `categoryFieldApplicability(passport, template.category)`. Pass `{}` for
|
|
128
|
+
* non-battery categories — the gate branch is then a no-op. Keeping it
|
|
129
|
+
* injected preserves the IO-free/pure contract of this function.
|
|
130
|
+
*/
|
|
131
|
+
export function evaluateFieldRequirements(passport, template, applicabilityMap = {}) {
|
|
132
|
+
// The passport's sub-category, read from the field the template category
|
|
133
|
+
// names (battery: batteryCategory, fmcg: productSubcategory, detergents:
|
|
134
|
+
// detergentUserType; core SUBCATEGORY_FIELD), so effectiveRequired can
|
|
135
|
+
// resolve `requiredBy`. Unset → undefined → the plain `required` flag.
|
|
136
|
+
const category = passportSubcategory(template.category, passport.fields);
|
|
137
|
+
const missingFields = [];
|
|
138
|
+
const unapprovedFields = [];
|
|
139
|
+
const conditionalMissingFields = [];
|
|
140
|
+
for (const tf of template.fields) {
|
|
141
|
+
// `anticipated` fields rest on a delegated act that has not been adopted,
|
|
142
|
+
// so no instrument in force compels the data and their absence cannot block
|
|
143
|
+
// publication. Skip them before the required check.
|
|
144
|
+
if (tf.validation.anticipated)
|
|
145
|
+
continue;
|
|
146
|
+
const f = passport.fields[tf.key];
|
|
147
|
+
// ── Gate-aware conditional check ────────────────────────────────────────
|
|
148
|
+
// A field with requiredBy[category] === "conditional" is normally not
|
|
149
|
+
// required. When a gate's APPROVED flag confirms the condition applies,
|
|
150
|
+
// the field becomes required and its absence is a HARD publish block.
|
|
151
|
+
// An unapproved value (pending_review) is also a hard block — consistent
|
|
152
|
+
// with the owner's decision that conditional duties are non-bypassable.
|
|
153
|
+
// "unknown" (absent or pending_review FLAG) → skip here; the rules engine
|
|
154
|
+
// emits an unverifiable_conditional warning via BAT-APP but never blocks.
|
|
155
|
+
const rb = tf.validation.requiredBy;
|
|
156
|
+
if (applicabilityMap[tf.key] === "applies" &&
|
|
157
|
+
rb &&
|
|
158
|
+
category !== undefined &&
|
|
159
|
+
rb[category] === "conditional") {
|
|
160
|
+
if (isFieldAbsent(f)) {
|
|
161
|
+
conditionalMissingFields.push(tf.key);
|
|
162
|
+
}
|
|
163
|
+
else if (f.status !== "approved") {
|
|
164
|
+
// Field is present but not yet confirmed — same hard block as absent,
|
|
165
|
+
// because an unreviewed value satisfies neither the owner nor the law.
|
|
166
|
+
conditionalMissingFields.push(tf.key);
|
|
167
|
+
}
|
|
168
|
+
// Don't fall through to the static required check — the gate owns this
|
|
169
|
+
// field's requirement for this passport.
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// ── Static required check ────────────────────────────────────────────────
|
|
173
|
+
if (!effectiveRequiredFor(template.category, tf, category))
|
|
174
|
+
continue;
|
|
175
|
+
// Shared helper: `[]` stays a valid answer ("none apply"), while a
|
|
176
|
+
// whitespace-only string now counts as missing — the inline `=== ""` it
|
|
177
|
+
// replaces let " " satisfy a required field and publish a blank.
|
|
178
|
+
if (isFieldAbsent(f)) {
|
|
179
|
+
missingFields.push(tf.key);
|
|
180
|
+
}
|
|
181
|
+
else if (f.status !== "approved") {
|
|
182
|
+
unapprovedFields.push(tf.key);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { missingFields, unapprovedFields, conditionalMissingFields };
|
|
186
|
+
}
|
|
113
187
|
/**
|
|
114
188
|
* Returns whether `passport` satisfies every publish precondition given
|
|
115
189
|
* its `template`. A passport is publish-ready iff:
|
|
@@ -117,16 +191,27 @@ export function notApplicableForCategory(tf, category) {
|
|
|
117
191
|
* states return ready:false with a specific reason),
|
|
118
192
|
* - every template field whose effective required flag (resolved via
|
|
119
193
|
* `effectiveRequired` — which consults `requiredBy` when present) is
|
|
120
|
-
* true has a non-empty value AND status === "approved"
|
|
194
|
+
* true has a non-empty value AND status === "approved",
|
|
195
|
+
* - every `requiredBy[category] === "conditional"` field whose gate
|
|
196
|
+
* resolves `"applies"` (from an APPROVED condition flag) also has a
|
|
197
|
+
* non-empty value AND status === "approved" (hard block, same severity
|
|
198
|
+
* as a structural blocker).
|
|
121
199
|
*
|
|
122
200
|
* `undefined` template means we can't evaluate — treat as not-ready.
|
|
201
|
+
*
|
|
202
|
+
* @param applicabilityMap Pre-computed gate-applicability map (field key →
|
|
203
|
+
* "applies" | "not_applicable" | "unknown"). Computed by the caller using
|
|
204
|
+
* `categoryFieldApplicability(passport, template.category)`. Pass `{}` or
|
|
205
|
+
* omit for non-battery passports (no change in behaviour). Keeping it
|
|
206
|
+
* injected preserves the IO-free/pure contract of this function.
|
|
123
207
|
*/
|
|
124
|
-
export function checkPublishReady(passport, template) {
|
|
208
|
+
export function checkPublishReady(passport, template, applicabilityMap = {}) {
|
|
125
209
|
if (passport.status === "published") {
|
|
126
210
|
return {
|
|
127
211
|
ready: false,
|
|
128
212
|
missingFields: [],
|
|
129
213
|
unapprovedFields: [],
|
|
214
|
+
conditionalMissingFields: [],
|
|
130
215
|
reason: "Already published",
|
|
131
216
|
blockerType: "hard",
|
|
132
217
|
};
|
|
@@ -136,6 +221,7 @@ export function checkPublishReady(passport, template) {
|
|
|
136
221
|
ready: false,
|
|
137
222
|
missingFields: [],
|
|
138
223
|
unapprovedFields: [],
|
|
224
|
+
conditionalMissingFields: [],
|
|
139
225
|
reason: `Cannot publish from status ${passport.status}`,
|
|
140
226
|
blockerType: "hard",
|
|
141
227
|
};
|
|
@@ -145,50 +231,38 @@ export function checkPublishReady(passport, template) {
|
|
|
145
231
|
ready: false,
|
|
146
232
|
missingFields: [],
|
|
147
233
|
unapprovedFields: [],
|
|
234
|
+
conditionalMissingFields: [],
|
|
148
235
|
reason: "Template missing",
|
|
149
236
|
blockerType: "hard",
|
|
150
237
|
};
|
|
151
238
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// publication. Skip them before the required check.
|
|
163
|
-
if (tf.validation.anticipated)
|
|
164
|
-
continue;
|
|
165
|
-
if (!effectiveRequiredFor(template.category, tf, category))
|
|
166
|
-
continue;
|
|
167
|
-
const f = passport.fields[tf.key];
|
|
168
|
-
// Shared helper: `[]` stays a valid answer ("none apply"), while a
|
|
169
|
-
// whitespace-only string now counts as missing — the inline `=== ""` it
|
|
170
|
-
// replaces let " " satisfy a required field and publish a blank.
|
|
171
|
-
if (isFieldAbsent(f)) {
|
|
172
|
-
missingFields.push(tf.key);
|
|
173
|
-
}
|
|
174
|
-
else if (f.status !== "approved") {
|
|
175
|
-
unapprovedFields.push(tf.key);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (missingFields.length === 0 && unapprovedFields.length === 0) {
|
|
179
|
-
return { ready: true, missingFields: [], unapprovedFields: [] };
|
|
239
|
+
const { missingFields, unapprovedFields, conditionalMissingFields } = evaluateFieldRequirements(passport, template, applicabilityMap);
|
|
240
|
+
if (missingFields.length === 0 &&
|
|
241
|
+
unapprovedFields.length === 0 &&
|
|
242
|
+
conditionalMissingFields.length === 0) {
|
|
243
|
+
return {
|
|
244
|
+
ready: true,
|
|
245
|
+
missingFields: [],
|
|
246
|
+
unapprovedFields: [],
|
|
247
|
+
conditionalMissingFields: [],
|
|
248
|
+
};
|
|
180
249
|
}
|
|
181
250
|
const parts = [];
|
|
251
|
+
if (conditionalMissingFields.length > 0)
|
|
252
|
+
parts.push(`${conditionalMissingFields.length} conditional-required missing`);
|
|
182
253
|
if (missingFields.length > 0)
|
|
183
254
|
parts.push(`${missingFields.length} missing`);
|
|
184
255
|
if (unapprovedFields.length > 0)
|
|
185
256
|
parts.push(`${unapprovedFields.length} not approved`);
|
|
257
|
+
// Hard block when any conditional-required field is missing; soft gap otherwise.
|
|
258
|
+
const blockerType = conditionalMissingFields.length > 0 ? "conditional" : "gap";
|
|
186
259
|
return {
|
|
187
260
|
ready: false,
|
|
188
261
|
missingFields,
|
|
189
262
|
unapprovedFields,
|
|
263
|
+
conditionalMissingFields,
|
|
190
264
|
reason: `Required fields: ${parts.join(", ")}`,
|
|
191
|
-
blockerType
|
|
265
|
+
blockerType,
|
|
192
266
|
};
|
|
193
267
|
}
|
|
194
268
|
//# sourceMappingURL=publish-gate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-gate.js","sourceRoot":"","sources":["../../src/vendor/publish-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"publish-gate.js","sourceRoot":"","sources":["../../src/vendor/publish-gate.ts"],"names":[],"mappings":"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"}
|
package/dist/verdict.d.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* Pure and IO-free: no database, no network, no clock.
|
|
10
10
|
*
|
|
11
11
|
* Composition:
|
|
12
|
-
* static tier — required fields
|
|
13
|
-
*
|
|
12
|
+
* static tier — evaluateFieldRequirements (required fields,
|
|
13
|
+
* status-independent) + required economic operators
|
|
14
|
+
* (required-roles) + format validation
|
|
14
15
|
* conditional tier — CONDITIONAL_RULES[category] + CROSS_CUTTING_RULES
|
|
15
16
|
* coverage — "evaluated" if the category has conditional rules,
|
|
16
17
|
* else "static-only"
|
package/dist/verdict.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verdict.d.ts","sourceRoot":"","sources":["../src/verdict.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"verdict.d.ts","sourceRoot":"","sources":["../src/verdict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAiB,MAAM,sBAAsB,CAAC;AAM9E,OAAO,KAAK,EAEV,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AAiEpB;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,QAAQ,EAAE,MAAM;AAChB,kFAAkF;AAClF,GAAG,GAAE,IAAiB,GACrB,gBAAgB,CA6IlB"}
|
package/dist/verdict.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* Pure and IO-free: no database, no network, no clock.
|
|
10
10
|
*
|
|
11
11
|
* Composition:
|
|
12
|
-
* static tier — required fields
|
|
13
|
-
*
|
|
12
|
+
* static tier — evaluateFieldRequirements (required fields,
|
|
13
|
+
* status-independent) + required economic operators
|
|
14
|
+
* (required-roles) + format validation
|
|
14
15
|
* conditional tier — CONDITIONAL_RULES[category] + CROSS_CUTTING_RULES
|
|
15
16
|
* coverage — "evaluated" if the category has conditional rules,
|
|
16
17
|
* else "static-only"
|
|
@@ -18,9 +19,10 @@
|
|
|
18
19
|
* Each finding cites the article of the instrument that mandates it, taken
|
|
19
20
|
* from the template field's own `regulationRef`.
|
|
20
21
|
*/
|
|
21
|
-
import {
|
|
22
|
+
import { evaluateFieldRequirements } from "./vendor/publish-gate.js";
|
|
22
23
|
import { getPartyRoles } from "./vendor/required-roles.js";
|
|
23
24
|
import { derivePassportCounts } from "./vendor/counts.js";
|
|
25
|
+
import { categoryFieldApplicability } from "./vendor/battery-applicability.js";
|
|
24
26
|
import { CONDITIONAL_RULES, CROSS_CUTTING_RULES } from "./rules.js";
|
|
25
27
|
/** Validate a filled value against a template field's format rules.
|
|
26
28
|
* Returns a finding (warning) on mismatch, or null when clean. Empty
|
|
@@ -113,11 +115,17 @@ now = new Date()) {
|
|
|
113
115
|
const critical = [];
|
|
114
116
|
const warnings = [];
|
|
115
117
|
const checkedRules = [];
|
|
116
|
-
// ── Static tier 1: required fields
|
|
118
|
+
// ── Static tier 1: required fields ─────────────────────────────────
|
|
117
119
|
checkedRules.push("static:required-fields");
|
|
118
|
-
|
|
120
|
+
// Compute the gate-applicability map for conditional duties whose APPROVED
|
|
121
|
+
// flag confirms the condition holds. evaluateFieldRequirements is called
|
|
122
|
+
// directly (not via checkPublishReady) so published passports are not
|
|
123
|
+
// short-circuited — live DPPs must be re-evaluated immediately when a
|
|
124
|
+
// field or flag changes.
|
|
125
|
+
const applicabilityMap = categoryFieldApplicability(passport, category);
|
|
126
|
+
const { missingFields, unapprovedFields, conditionalMissingFields } = evaluateFieldRequirements(passport, template, applicabilityMap);
|
|
119
127
|
const fieldRef = (key) => template.fields.find((f) => f.key === key)?.regulationRef;
|
|
120
|
-
for (const key of
|
|
128
|
+
for (const key of missingFields) {
|
|
121
129
|
const ref = fieldRef(key);
|
|
122
130
|
critical.push({
|
|
123
131
|
type: "missing_field",
|
|
@@ -129,7 +137,7 @@ now = new Date()) {
|
|
|
129
137
|
fix: `Provide a value for ${key}.`,
|
|
130
138
|
});
|
|
131
139
|
}
|
|
132
|
-
for (const key of
|
|
140
|
+
for (const key of unapprovedFields) {
|
|
133
141
|
critical.push({
|
|
134
142
|
type: "unapproved_field",
|
|
135
143
|
severity: "critical",
|
|
@@ -138,6 +146,33 @@ now = new Date()) {
|
|
|
138
146
|
fix: `Review and approve ${key}.`,
|
|
139
147
|
});
|
|
140
148
|
}
|
|
149
|
+
// Gate-confirmed conditional duties: the APPROVED flag makes the duty concrete
|
|
150
|
+
// and the field must be present + approved. Same hard-block severity as a
|
|
151
|
+
// statically-required field — the owner confirmed the condition holds.
|
|
152
|
+
// "gate:conditional" is pushed when the category has gates (non-empty
|
|
153
|
+
// applicabilityMap) so the caller knows whether gates were evaluated.
|
|
154
|
+
if (Object.keys(applicabilityMap).length > 0) {
|
|
155
|
+
checkedRules.push("gate:conditional");
|
|
156
|
+
}
|
|
157
|
+
for (const key of conditionalMissingFields) {
|
|
158
|
+
const ref = fieldRef(key);
|
|
159
|
+
const f = passport.fields[key];
|
|
160
|
+
const isAbsent = f == null || f.value === null || f.value === undefined || f.value === "";
|
|
161
|
+
critical.push({
|
|
162
|
+
type: "missing_field",
|
|
163
|
+
severity: "critical",
|
|
164
|
+
ruleId: "gate:conditional",
|
|
165
|
+
target: key,
|
|
166
|
+
regulation: ref ? template.regulation?.number : undefined,
|
|
167
|
+
article: ref?.article ?? undefined,
|
|
168
|
+
why: isAbsent
|
|
169
|
+
? `Conditional field "${key}" is required because an approved condition flag confirms the duty applies, but the field has no value.`
|
|
170
|
+
: `Conditional field "${key}" is required because an approved condition flag confirms the duty applies, but the value is not yet approved.`,
|
|
171
|
+
fix: isAbsent
|
|
172
|
+
? `Provide and approve a value for ${key}.`
|
|
173
|
+
: `Approve the value for ${key}.`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
141
176
|
// ── Static tier 2: required economic-operator parties ──────────────
|
|
142
177
|
checkedRules.push("static:required-parties");
|
|
143
178
|
const roles = getPartyRoles(category);
|
package/dist/verdict.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verdict.js","sourceRoot":"","sources":["../src/verdict.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"verdict.js","sourceRoot":"","sources":["../src/verdict.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tracepass/dpp-validate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Evaluate an EU Digital Product Passport against its category field-spec — a three-tier compliance verdict with regulation-cited findings. Pure functions, no third-party runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"digital-product-passport",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"clean": "rm -rf dist"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@tracepass/dpp-types": "^0.
|
|
48
|
+
"@tracepass/dpp-types": "^0.13.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"typescript": "^5.6.3"
|