@tracepass/dpp-validate 0.1.0
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/LICENSE +202 -0
- package/README.md +79 -0
- package/dist/battery-applicability.d.ts +84 -0
- package/dist/battery-applicability.d.ts.map +1 -0
- package/dist/battery-applicability.js +175 -0
- package/dist/battery-applicability.js.map +1 -0
- package/dist/eu-countries.d.ts +24 -0
- package/dist/eu-countries.d.ts.map +1 -0
- package/dist/eu-countries.js +37 -0
- package/dist/eu-countries.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/rules.d.ts +40 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +376 -0
- package/dist/rules.js.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/vendor/counts.d.ts +32 -0
- package/dist/vendor/counts.d.ts.map +1 -0
- package/dist/vendor/counts.js +28 -0
- package/dist/vendor/counts.js.map +1 -0
- package/dist/vendor/publish-gate.d.ts +41 -0
- package/dist/vendor/publish-gate.d.ts.map +1 -0
- package/dist/vendor/publish-gate.js +85 -0
- package/dist/vendor/publish-gate.js.map +1 -0
- package/dist/vendor/required-roles.d.ts +72 -0
- package/dist/vendor/required-roles.d.ts.map +1 -0
- package/dist/vendor/required-roles.js +144 -0
- package/dist/vendor/required-roles.js.map +1 -0
- package/dist/verdict.d.ts +30 -0
- package/dist/verdict.d.ts.map +1 -0
- package/dist/verdict.js +189 -0
- package/dist/verdict.js.map +1 -0
- package/package.json +22 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluate a Digital Product Passport against its category field-spec.
|
|
3
|
+
*
|
|
4
|
+
* `evaluateCompliance` takes a passport and its template as plain objects and
|
|
5
|
+
* returns a three-tier verdict with regulation-cited findings. It reads no
|
|
6
|
+
* database and makes no network calls.
|
|
7
|
+
*/
|
|
8
|
+
export { evaluateCompliance } from "./verdict.js";
|
|
9
|
+
export { isEuEeaCountry } from "./eu-countries.js";
|
|
10
|
+
export { CONDITIONAL_RULES, CROSS_CUTTING_RULES, } from "./rules.js";
|
|
11
|
+
export { IN_SCOPE_BATTERY_CATEGORIES, BATTERY_FIELD_GATES, GATED_FIELD_KEYS, batteryFieldApplicability, gateForKey, } from "./battery-applicability.js";
|
|
12
|
+
// Re-exported because a passport's publish-readiness and required economic
|
|
13
|
+
// operators are part of judging whether it is compliant, not separate concerns.
|
|
14
|
+
export { checkPublishReady } from "./vendor/publish-gate.js";
|
|
15
|
+
export { getPartyRoles, isRequiredRole, allRolesForCategory, CATEGORY_PARTY_ROLES } from "./vendor/required-roles.js";
|
|
16
|
+
export { derivePassportCounts } from "./vendor/counts.js";
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAWlD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EACL,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,gBAAgB,EAChB,yBAAyB,EACzB,UAAU,GACX,MAAM,4BAA4B,CAAC;AAGpC,2EAA2E;AAC3E,gFAAgF;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAEtH,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-category CONDITIONAL compliance rules.
|
|
3
|
+
*
|
|
4
|
+
* These encode the conditional obligations that are binding and in force.
|
|
5
|
+
* Three categories carry them — battery, chemicals, construction — plus one
|
|
6
|
+
* cross-cutting rule (CC-1) that applies to every category. The remaining
|
|
7
|
+
* categories have no rule entry here, so the engine reports them
|
|
8
|
+
* `static-only`: their template's required fields are the whole obligation.
|
|
9
|
+
*
|
|
10
|
+
* Rules live in TypeScript rather than a JSON DSL in the templates.
|
|
11
|
+
* Regulatory logic stays in code, reviewed in pull requests, and is
|
|
12
|
+
* deliberately not drift-able by editing a template. Each rule cites its
|
|
13
|
+
* article so the verdict can show provenance.
|
|
14
|
+
*
|
|
15
|
+
* A rule returns findings. `unverifiable_conditional` (warning) is
|
|
16
|
+
* emitted when a rule's TRIGGER field is absent — we never silently
|
|
17
|
+
* skip a mandatory conditional, and never assert compliance on data we
|
|
18
|
+
* couldn't read.
|
|
19
|
+
*
|
|
20
|
+
* Tier discipline: `critical` is reserved for binding, in-force obligations
|
|
21
|
+
* traced to a primary source. A draft or not-yet-dated obligation is a
|
|
22
|
+
* `warning`, never a blocker.
|
|
23
|
+
*/
|
|
24
|
+
import type { Passport, Template } from "@tracepass/dpp-types";
|
|
25
|
+
import type { ComplianceFinding } from "./types.js";
|
|
26
|
+
/** A conditional rule: given passport+template, produce zero or more findings. */
|
|
27
|
+
export interface ConditionalRule {
|
|
28
|
+
id: string;
|
|
29
|
+
run(passport: Passport, template: Template): ComplianceFinding[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Conditional-rule registry. Categories ABSENT from this map have no
|
|
33
|
+
* binding conditionals in force → the engine reports them `static-only`.
|
|
34
|
+
* CC-1 runs for every category and is composed in by the engine, so it
|
|
35
|
+
* is NOT listed per-category here.
|
|
36
|
+
*/
|
|
37
|
+
export declare const CONDITIONAL_RULES: Record<string, ConditionalRule[]>;
|
|
38
|
+
/** The cross-cutting rule the engine runs for every category. */
|
|
39
|
+
export declare const CROSS_CUTTING_RULES: ConditionalRule[];
|
|
40
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAG/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAapD,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,iBAAiB,EAAE,CAAC;CAClE;AAwVD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAI/D,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,EAAE,eAAe,EAAU,CAAC"}
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-category CONDITIONAL compliance rules.
|
|
3
|
+
*
|
|
4
|
+
* These encode the conditional obligations that are binding and in force.
|
|
5
|
+
* Three categories carry them — battery, chemicals, construction — plus one
|
|
6
|
+
* cross-cutting rule (CC-1) that applies to every category. The remaining
|
|
7
|
+
* categories have no rule entry here, so the engine reports them
|
|
8
|
+
* `static-only`: their template's required fields are the whole obligation.
|
|
9
|
+
*
|
|
10
|
+
* Rules live in TypeScript rather than a JSON DSL in the templates.
|
|
11
|
+
* Regulatory logic stays in code, reviewed in pull requests, and is
|
|
12
|
+
* deliberately not drift-able by editing a template. Each rule cites its
|
|
13
|
+
* article so the verdict can show provenance.
|
|
14
|
+
*
|
|
15
|
+
* A rule returns findings. `unverifiable_conditional` (warning) is
|
|
16
|
+
* emitted when a rule's TRIGGER field is absent — we never silently
|
|
17
|
+
* skip a mandatory conditional, and never assert compliance on data we
|
|
18
|
+
* couldn't read.
|
|
19
|
+
*
|
|
20
|
+
* Tier discipline: `critical` is reserved for binding, in-force obligations
|
|
21
|
+
* traced to a primary source. A draft or not-yet-dated obligation is a
|
|
22
|
+
* `warning`, never a blocker.
|
|
23
|
+
*/
|
|
24
|
+
import { isEuEeaCountry } from "./eu-countries.js";
|
|
25
|
+
import { BATTERY_FIELD_GATES, batteryFieldApplicability } from "./battery-applicability.js";
|
|
26
|
+
/** A field counts as "present" when it exists with a non-empty value. */
|
|
27
|
+
function hasValue(passport, key) {
|
|
28
|
+
const f = passport.fields[key];
|
|
29
|
+
return !!f && f.value !== null && f.value !== undefined && f.value !== "";
|
|
30
|
+
}
|
|
31
|
+
/** Read a field's raw value (or undefined when absent/empty). */
|
|
32
|
+
function valueOf(passport, key) {
|
|
33
|
+
return hasValue(passport, key) ? passport.fields[key].value : undefined;
|
|
34
|
+
}
|
|
35
|
+
// ── CC-1 · cross-cutting EU economic-operator rule ──────────────────
|
|
36
|
+
// Reg (EU) 2019/1020 Art. 4(1)+(2): a product may be placed on the
|
|
37
|
+
// market only if there is an EU-established operator. When the
|
|
38
|
+
// manufacturer is non-EU, that operator must be one of importer / AR /
|
|
39
|
+
// fulfilment service provider. This is GROUPED (any one satisfies) —
|
|
40
|
+
// NOT "AR required". We have no fulfilmentServiceProvider role, so the
|
|
41
|
+
// satisfying set is {importer, authorisedRepresentative}; if neither is
|
|
42
|
+
// present we warn (not critical) and point at the FSP escape hatch, to
|
|
43
|
+
// avoid a false-positive on a legitimately FSP-only arrangement.
|
|
44
|
+
const CC1 = {
|
|
45
|
+
id: "CC-1",
|
|
46
|
+
run(passport) {
|
|
47
|
+
const mfr = passport.parties?.manufacturer;
|
|
48
|
+
// Trigger field absent → can't evaluate. Warn, don't assert.
|
|
49
|
+
if (!mfr || !mfr.country) {
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
type: "unverifiable_conditional",
|
|
53
|
+
severity: "warning",
|
|
54
|
+
target: "manufacturer.country",
|
|
55
|
+
regulation: "(EU) 2019/1020",
|
|
56
|
+
article: "Art. 4",
|
|
57
|
+
ruleId: "CC-1",
|
|
58
|
+
why: "Manufacturer country is not set, so the EU economic-operator requirement couldn't be evaluated.",
|
|
59
|
+
fix: "Set the manufacturer party's country so compliance with Art. 4 can be confirmed.",
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
if (isEuEeaCountry(mfr.country))
|
|
64
|
+
return []; // EU/EEA manufacturer satisfies Art. 4 directly.
|
|
65
|
+
const satisfied = !!passport.parties?.importer || !!passport.parties?.authorisedRepresentative;
|
|
66
|
+
if (satisfied)
|
|
67
|
+
return [];
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
type: "conditional_missing",
|
|
71
|
+
severity: "warning",
|
|
72
|
+
target: "importer|authorisedRepresentative",
|
|
73
|
+
regulation: "(EU) 2019/1020",
|
|
74
|
+
article: "Art. 4(1)(2)",
|
|
75
|
+
ruleId: "CC-1",
|
|
76
|
+
why: `Manufacturer is established outside the EU/EEA (${mfr.country}); EU placing on market requires an EU-established operator.`,
|
|
77
|
+
fix: "Add an importer OR an authorised representative (or record a fulfilment service provider) established in the EU.",
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
// ── Battery · Reg (EU) 2023/1542 ────────────────────────────────────
|
|
83
|
+
// BAT-1: battery passport required IF category ∈ {LMT, EV, industrial>2kWh}.
|
|
84
|
+
// The template's `batteryCategory` enum already encodes the scope split
|
|
85
|
+
// (LMT, EV, industrial_gt_2kwh vs industrial_lte_2kwh, SLI, portable),
|
|
86
|
+
// so the ">2 kWh" line is an enum check, not kWh math.
|
|
87
|
+
const IN_SCOPE_BATTERY = new Set(["LMT", "EV", "industrial_gt_2kwh"]);
|
|
88
|
+
// Fields the passport must carry once it's an in-scope battery. Kept tight —
|
|
89
|
+
// only the identifiers Art. 77 hangs the passport on; broader field
|
|
90
|
+
// completeness is the static tier's job.
|
|
91
|
+
const BATTERY_PASSPORT_FIELDS = ["batteryUniqueIdentifier"];
|
|
92
|
+
const BAT1 = {
|
|
93
|
+
id: "BAT-1",
|
|
94
|
+
run(passport) {
|
|
95
|
+
const cat = valueOf(passport, "batteryCategory");
|
|
96
|
+
if (cat === undefined) {
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
type: "unverifiable_conditional",
|
|
100
|
+
severity: "warning",
|
|
101
|
+
target: "batteryCategory",
|
|
102
|
+
regulation: "(EU) 2023/1542",
|
|
103
|
+
article: "Art. 77(1)",
|
|
104
|
+
ruleId: "BAT-1",
|
|
105
|
+
why: "Battery category isn't set, so battery-passport scope (LMT / EV / industrial >2 kWh) couldn't be evaluated.",
|
|
106
|
+
fix: "Set batteryCategory. Portable and SLI batteries are out of scope; LMT, EV and industrial >2 kWh require a battery passport.",
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
// Out-of-scope categories (portable, SLI, industrial_lte_2kwh) → no obligation.
|
|
111
|
+
if (!IN_SCOPE_BATTERY.has(String(cat)))
|
|
112
|
+
return [];
|
|
113
|
+
const findings = [];
|
|
114
|
+
for (const key of BATTERY_PASSPORT_FIELDS) {
|
|
115
|
+
if (!hasValue(passport, key)) {
|
|
116
|
+
findings.push({
|
|
117
|
+
type: "conditional_missing",
|
|
118
|
+
severity: "critical",
|
|
119
|
+
target: key,
|
|
120
|
+
regulation: "(EU) 2023/1542",
|
|
121
|
+
article: "Art. 77",
|
|
122
|
+
ruleId: "BAT-1",
|
|
123
|
+
why: `This is an in-scope battery (${String(cat)}); a battery passport with its unique identifier is mandatory.`,
|
|
124
|
+
fix: `Provide ${key} — the battery passport's unique identifier (GS1 Digital Link).`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return findings;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
// ── Battery · field-applicability (deck 27 May 2026, slide 36) ──────
|
|
132
|
+
// BAT-APP: not every Annex XIII field applies to every battery. The
|
|
133
|
+
// shared predicate (battery-applicability.ts) decides per field from
|
|
134
|
+
// batteryCategory + the batteryProfile classifiers. This rule turns that
|
|
135
|
+
// into findings WITHOUT ever blocking:
|
|
136
|
+
// - a field that's `not_applicable` but FILLED → warning (likely
|
|
137
|
+
// mis-entered or the battery is mis-classified). Never critical —
|
|
138
|
+
// extra data isn't a compliance failure, just a flag.
|
|
139
|
+
// - a gate whose trigger is absent (`unknown`) AND the dependent field
|
|
140
|
+
// is filled → unverifiable_conditional warning (can't confirm the
|
|
141
|
+
// field belongs; ask for the missing classifier).
|
|
142
|
+
// We deliberately do NOT emit "missing applicable field" here — required-
|
|
143
|
+
// field completeness is the static tier's job; this rule is purely about
|
|
144
|
+
// applicability, so it stays additive and can't double-count.
|
|
145
|
+
const BAT_APP = {
|
|
146
|
+
id: "BAT-APP",
|
|
147
|
+
run(passport) {
|
|
148
|
+
const findings = [];
|
|
149
|
+
// Reuse the shared predicate so the verdict + editor read applicability
|
|
150
|
+
// through ONE path (incl. the "pending AI flag = unknown" safety rule).
|
|
151
|
+
const applicability = batteryFieldApplicability(passport, "battery");
|
|
152
|
+
for (const gate of BATTERY_FIELD_GATES) {
|
|
153
|
+
// Every key in a gate shares the gate's verdict; sample the first.
|
|
154
|
+
const verdict = applicability[gate.keys[0]];
|
|
155
|
+
if (verdict === "applies")
|
|
156
|
+
continue;
|
|
157
|
+
for (const key of gate.keys) {
|
|
158
|
+
const filled = hasValue(passport, key);
|
|
159
|
+
if (verdict === "not_applicable" && filled) {
|
|
160
|
+
findings.push({
|
|
161
|
+
type: "invalid_format",
|
|
162
|
+
severity: "warning",
|
|
163
|
+
target: key,
|
|
164
|
+
regulation: "(EU) 2023/1542",
|
|
165
|
+
article: gate.article,
|
|
166
|
+
ruleId: "BAT-APP",
|
|
167
|
+
why: `${key} is filled but doesn't apply to this battery — ${gate.reason}`,
|
|
168
|
+
fix: `Remove ${key}, or correct the battery classification (batteryCategory / battery profile) if this field should apply.`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else if (verdict === "unknown" && filled) {
|
|
172
|
+
findings.push({
|
|
173
|
+
type: "unverifiable_conditional",
|
|
174
|
+
severity: "warning",
|
|
175
|
+
target: key,
|
|
176
|
+
regulation: "(EU) 2023/1542",
|
|
177
|
+
article: gate.article,
|
|
178
|
+
ruleId: "BAT-APP",
|
|
179
|
+
why: `${key} is filled, but whether it applies couldn't be confirmed — ${gate.reason}`,
|
|
180
|
+
fix: "Set the battery classification (has-BMS / rechargeable / external-storage) so applicability can be confirmed.",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return findings;
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
// ── Battery · value validation (not just presence) ──────────────────
|
|
189
|
+
// BAT-VAL: the static tier + BAT-1 check fields are PRESENT; this rule
|
|
190
|
+
// checks the values that ARE present are PLAUSIBLE. All warnings, never
|
|
191
|
+
// blocking — a malformed value is a data-quality flag for the reviewer,
|
|
192
|
+
// not a hard compliance failure (the reviewer owns the value per the
|
|
193
|
+
// "AI suggests, human approves" product stance). Two kinds of check:
|
|
194
|
+
// (1) numeric-range: percentages must be 0–100; carbon footprints ≥ 0.
|
|
195
|
+
// (2) correlated-requirement: a recorded hazardous substance engages
|
|
196
|
+
// the REACH/SCIP notification obligation, so scipNotificationNumber
|
|
197
|
+
// should be present.
|
|
198
|
+
// Absent fields are skipped here (presence is BAT-1 / the static tier's
|
|
199
|
+
// job) so this rule stays additive and can't double-count.
|
|
200
|
+
/** Parse a field value as a finite number, or undefined if not numeric. */
|
|
201
|
+
function numberOf(passport, key) {
|
|
202
|
+
if (!hasValue(passport, key))
|
|
203
|
+
return undefined;
|
|
204
|
+
const raw = passport.fields[key].value;
|
|
205
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
206
|
+
return Number.isFinite(n) ? n : undefined;
|
|
207
|
+
}
|
|
208
|
+
// Percentage fields that must sit within 0–100 when present.
|
|
209
|
+
const BATTERY_PERCENT_FIELDS = [
|
|
210
|
+
"recycledContentCobalt",
|
|
211
|
+
"recycledContentLithium",
|
|
212
|
+
"recycledContentNickel",
|
|
213
|
+
"recycledContentLead",
|
|
214
|
+
"stateOfHealth",
|
|
215
|
+
"stateOfCertifiedEnergy",
|
|
216
|
+
];
|
|
217
|
+
// Carbon-footprint fields that must be ≥ 0 when present (kg CO2e/kWh).
|
|
218
|
+
const BATTERY_NONNEGATIVE_FIELDS = [
|
|
219
|
+
"carbonFootprintTotal",
|
|
220
|
+
"cfRawMaterialAcquisition",
|
|
221
|
+
"cfMainProductProduction",
|
|
222
|
+
"cfDistribution",
|
|
223
|
+
"cfEndOfLifeRecycling",
|
|
224
|
+
];
|
|
225
|
+
const BAT_VAL = {
|
|
226
|
+
id: "BAT-VAL",
|
|
227
|
+
run(passport) {
|
|
228
|
+
const findings = [];
|
|
229
|
+
// (1a) percentages in [0, 100].
|
|
230
|
+
for (const key of BATTERY_PERCENT_FIELDS) {
|
|
231
|
+
const n = numberOf(passport, key);
|
|
232
|
+
if (n !== undefined && (n < 0 || n > 100)) {
|
|
233
|
+
findings.push({
|
|
234
|
+
type: "invalid_format",
|
|
235
|
+
severity: "warning",
|
|
236
|
+
target: key,
|
|
237
|
+
regulation: "(EU) 2023/1542",
|
|
238
|
+
article: "Annex XIII",
|
|
239
|
+
ruleId: "BAT-VAL",
|
|
240
|
+
why: `${key} is ${n}, outside the valid 0–100% range.`,
|
|
241
|
+
fix: `Correct ${key} to a percentage between 0 and 100.`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// (1b) carbon-footprint values ≥ 0.
|
|
246
|
+
for (const key of BATTERY_NONNEGATIVE_FIELDS) {
|
|
247
|
+
const n = numberOf(passport, key);
|
|
248
|
+
if (n !== undefined && n < 0) {
|
|
249
|
+
findings.push({
|
|
250
|
+
type: "invalid_format",
|
|
251
|
+
severity: "warning",
|
|
252
|
+
target: key,
|
|
253
|
+
regulation: "(EU) 2023/1542",
|
|
254
|
+
article: "Art. 7",
|
|
255
|
+
ruleId: "BAT-VAL",
|
|
256
|
+
why: `${key} is ${n}; a carbon-footprint value cannot be negative.`,
|
|
257
|
+
fix: `Correct ${key} to a non-negative value (kg CO2e/kWh).`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// (2) correlated requirement: hazardous substance recorded ⇒ SCIP
|
|
262
|
+
// notification expected. Skipped entirely when no hazardous substance
|
|
263
|
+
// is recorded (obligation not engaged) — so it never fires spuriously.
|
|
264
|
+
const hazardous = valueOf(passport, "hazardousSubstances");
|
|
265
|
+
const hazardousList = Array.isArray(hazardous)
|
|
266
|
+
? hazardous
|
|
267
|
+
: hazardous !== undefined
|
|
268
|
+
? [hazardous]
|
|
269
|
+
: [];
|
|
270
|
+
if (hazardousList.length > 0 && !hasValue(passport, "scipNotificationNumber")) {
|
|
271
|
+
findings.push({
|
|
272
|
+
type: "conditional_missing",
|
|
273
|
+
severity: "warning",
|
|
274
|
+
target: "scipNotificationNumber",
|
|
275
|
+
regulation: "(EC) 1907/2006",
|
|
276
|
+
article: "WFD Art. 9(1)(i)",
|
|
277
|
+
ruleId: "BAT-VAL",
|
|
278
|
+
why: "A hazardous substance is recorded, which engages the SCIP notification obligation, but no SCIP notification number is set.",
|
|
279
|
+
fix: "Record scipNotificationNumber for the SCIP database notification covering the recorded hazardous substance(s).",
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return findings;
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
// ── Chemicals · REACH Art. 33 + SCIP (WFD Art. 9(1)(i)) ─────────────
|
|
286
|
+
// CHEM-1: SVHC disclosure required IF any Candidate-List SVHC present
|
|
287
|
+
// >0.1% w/w. The trigger is composition data the passport may not carry
|
|
288
|
+
// machine-readably — when `svhcSubstances` is absent we cannot tell
|
|
289
|
+
// whether the threshold is crossed, so we WARN (verify manually) rather
|
|
290
|
+
// than assert compliance. When the array is present and non-empty, the
|
|
291
|
+
// disclosure obligation is engaged → require the safe-use disclosure.
|
|
292
|
+
const CHEM1 = {
|
|
293
|
+
id: "CHEM-1",
|
|
294
|
+
run(passport) {
|
|
295
|
+
const svhc = valueOf(passport, "svhcSubstances");
|
|
296
|
+
if (svhc === undefined) {
|
|
297
|
+
return [
|
|
298
|
+
{
|
|
299
|
+
type: "unverifiable_conditional",
|
|
300
|
+
severity: "warning",
|
|
301
|
+
target: "svhcSubstances",
|
|
302
|
+
regulation: "(EC) 1907/2006",
|
|
303
|
+
article: "Art. 33",
|
|
304
|
+
ruleId: "CHEM-1",
|
|
305
|
+
why: "SVHC content isn't recorded, so REACH Art. 33 / SCIP disclosure (triggered above 0.1% w/w) couldn't be evaluated.",
|
|
306
|
+
fix: "Record svhcSubstances. If any Candidate-List substance exceeds 0.1% w/w, safe-use info and a SCIP notification are required.",
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
// Empty array → declared no SVHC above threshold → obligation not engaged.
|
|
311
|
+
const list = Array.isArray(svhc) ? svhc : [svhc];
|
|
312
|
+
if (list.length === 0)
|
|
313
|
+
return [];
|
|
314
|
+
// SVHC present → the safe-use disclosure (substance name min.) must be set.
|
|
315
|
+
if (!hasValue(passport, "svhcSubstanceName")) {
|
|
316
|
+
return [
|
|
317
|
+
{
|
|
318
|
+
type: "conditional_missing",
|
|
319
|
+
severity: "critical",
|
|
320
|
+
target: "svhcSubstanceName",
|
|
321
|
+
regulation: "(EC) 1907/2006",
|
|
322
|
+
article: "Art. 33(1)",
|
|
323
|
+
ruleId: "CHEM-1",
|
|
324
|
+
why: "A Candidate-List SVHC is recorded; Art. 33 requires safe-use information including, as a minimum, the substance name.",
|
|
325
|
+
fix: "Provide svhcSubstanceName and ensure a SCIP notification reference (WFD Art. 9(1)(i)) is recorded.",
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
}
|
|
329
|
+
return [];
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
// ── Construction · Reg (EU) 2024/3110 (new CPR) ─────────────────────
|
|
333
|
+
// CON-1: a Declaration of Performance & Conformity is required IF the
|
|
334
|
+
// product is covered by a harmonised technical specification. The
|
|
335
|
+
// template's `harmonizedStandardReference` (required) signals coverage;
|
|
336
|
+
// when it's set, the CE-marking / declaration evidence must follow.
|
|
337
|
+
// NB: the CPR's *DPP* provisions are NOT in force (future delegated
|
|
338
|
+
// acts) — only this DoP/DoC conditional is binding today.
|
|
339
|
+
const CON1 = {
|
|
340
|
+
id: "CON-1",
|
|
341
|
+
run(passport) {
|
|
342
|
+
const hsRef = valueOf(passport, "harmonizedStandardReference");
|
|
343
|
+
// No harmonised spec referenced → DoP/DoC obligation not engaged here.
|
|
344
|
+
if (hsRef === undefined)
|
|
345
|
+
return [];
|
|
346
|
+
if (!hasValue(passport, "ceMarkingStatus")) {
|
|
347
|
+
return [
|
|
348
|
+
{
|
|
349
|
+
type: "conditional_missing",
|
|
350
|
+
severity: "critical",
|
|
351
|
+
target: "ceMarkingStatus",
|
|
352
|
+
regulation: "(EU) 2024/3110",
|
|
353
|
+
article: "DoP/DoC",
|
|
354
|
+
ruleId: "CON-1",
|
|
355
|
+
why: "Product is covered by a harmonised technical specification; a Declaration of Performance & Conformity (CE marking) is required.",
|
|
356
|
+
fix: "Record ceMarkingStatus and the declaration evidence for the referenced harmonised standard.",
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
return [];
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* Conditional-rule registry. Categories ABSENT from this map have no
|
|
365
|
+
* binding conditionals in force → the engine reports them `static-only`.
|
|
366
|
+
* CC-1 runs for every category and is composed in by the engine, so it
|
|
367
|
+
* is NOT listed per-category here.
|
|
368
|
+
*/
|
|
369
|
+
export const CONDITIONAL_RULES = {
|
|
370
|
+
battery: [BAT1, BAT_APP, BAT_VAL],
|
|
371
|
+
chemicals: [CHEM1],
|
|
372
|
+
construction: [CON1],
|
|
373
|
+
};
|
|
374
|
+
/** The cross-cutting rule the engine runs for every category. */
|
|
375
|
+
export const CROSS_CUTTING_RULES = [CC1];
|
|
376
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the compliance-verdict engine.
|
|
3
|
+
*
|
|
4
|
+
* The engine answers, for one passport: "is this DPP compliant, and if not,
|
|
5
|
+
* what's missing — citing the regulation?" It composes three tiers of check:
|
|
6
|
+
*
|
|
7
|
+
* 1. STATIC — required template fields present + approved, required
|
|
8
|
+
* economic-operator parties present, filled values well-
|
|
9
|
+
* formed (pattern/enum/bounds). Applies to every category.
|
|
10
|
+
* 2. CONDITIONAL — per-category rules that only fire under a condition
|
|
11
|
+
* (e.g. battery passport required IF category is EV/LMT/
|
|
12
|
+
* industrial>2kWh; EU operator required IF manufacturer
|
|
13
|
+
* non-EU). Three categories carry binding conditionals —
|
|
14
|
+
* battery, chemicals, construction — plus one cross-cutting
|
|
15
|
+
* rule that applies to every category.
|
|
16
|
+
* 3. COVERAGE — the engine self-reports whether conditionals were
|
|
17
|
+
* evaluated or the category is static-only, so an agent
|
|
18
|
+
* NEVER reads silence as "compliant".
|
|
19
|
+
*
|
|
20
|
+
* Severity → verdict:
|
|
21
|
+
* - any `critical` → "incomplete"
|
|
22
|
+
* - else any `warning` → "compliant_with_warnings"
|
|
23
|
+
* - else → "compliant"
|
|
24
|
+
*/
|
|
25
|
+
export type ComplianceVerdict = "compliant" | "compliant_with_warnings" | "incomplete";
|
|
26
|
+
/** Did per-category conditional rules actually run for this passport's
|
|
27
|
+
* category, or does no binding conditional exist yet (static-only)? */
|
|
28
|
+
export type ConditionalCoverage = "evaluated" | "static-only";
|
|
29
|
+
export type FindingSeverity = "critical" | "warning";
|
|
30
|
+
export type FindingType =
|
|
31
|
+
/** A required template field has no value (static tier). */
|
|
32
|
+
"missing_field"
|
|
33
|
+
/** A field has a value but isn't approved yet (static tier). */
|
|
34
|
+
| "unapproved_field"
|
|
35
|
+
/** A required economic-operator party isn't set (static tier). */
|
|
36
|
+
| "missing_party"
|
|
37
|
+
/** A filled value violates the template's pattern/enum/bounds (static tier). */
|
|
38
|
+
| "invalid_format"
|
|
39
|
+
/** A conditional rule fired and its required field/party is missing. */
|
|
40
|
+
| "conditional_missing"
|
|
41
|
+
/** A conditional rule should apply but its trigger field is absent, so the
|
|
42
|
+
* engine couldn't evaluate it — surfaced as a warning, never silent. */
|
|
43
|
+
| "unverifiable_conditional";
|
|
44
|
+
export interface ComplianceFinding {
|
|
45
|
+
type: FindingType;
|
|
46
|
+
severity: FindingSeverity;
|
|
47
|
+
/** Field key or party role this finding concerns, when applicable. */
|
|
48
|
+
target?: string;
|
|
49
|
+
/** Regulation number, e.g. "(EU) 2023/1542". Present for conditional
|
|
50
|
+
* findings and for static findings whose field carries a regulationRef. */
|
|
51
|
+
regulation?: string;
|
|
52
|
+
/** Article / annex citation, e.g. "Art. 77" / "Annex VI". */
|
|
53
|
+
article?: string;
|
|
54
|
+
/** Short rule id from the verified spec (e.g. "BAT-1", "CC-1"). */
|
|
55
|
+
ruleId?: string;
|
|
56
|
+
/** One-line human explanation of why this matters. */
|
|
57
|
+
why: string;
|
|
58
|
+
/** One-line, actionable next step for the agent / user. */
|
|
59
|
+
fix?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface ComplianceResult {
|
|
62
|
+
verdict: ComplianceVerdict;
|
|
63
|
+
category: string;
|
|
64
|
+
conditionalCoverage: ConditionalCoverage;
|
|
65
|
+
/** Findings that force "incomplete". */
|
|
66
|
+
critical: ComplianceFinding[];
|
|
67
|
+
/** Findings that downgrade to "compliant_with_warnings" but don't block. */
|
|
68
|
+
warnings: ComplianceFinding[];
|
|
69
|
+
/** Rule ids that actually ran (static checks + every conditional rule
|
|
70
|
+
* evaluated). Lets an agent see WHAT was checked, not just the result. */
|
|
71
|
+
checkedRules: string[];
|
|
72
|
+
/** Completion metric, mirrors the passport's approved/total field ratio. */
|
|
73
|
+
completionPercentage: number;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,yBAAyB,GACzB,YAAY,CAAC;AAEjB;wEACwE;AACxE,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,aAAa,CAAC;AAE9D,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,SAAS,CAAC;AAErD,MAAM,MAAM,WAAW;AACrB,4DAA4D;AAC1D,eAAe;AACjB,gEAAgE;GAC9D,kBAAkB;AACpB,kEAAkE;GAChE,eAAe;AACjB,gFAAgF;GAC9E,gBAAgB;AAClB,wEAAwE;GACtE,qBAAqB;AACvB;yEACyE;GACvE,0BAA0B,CAAC;AAE/B,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,CAAC;IAC1B,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;gFAC4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,wCAAwC;IACxC,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,4EAA4E;IAC5E,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B;+EAC2E;IAC3E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,4EAA4E;IAC5E,oBAAoB,EAAE,MAAM,CAAC;CAC9B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the compliance-verdict engine.
|
|
3
|
+
*
|
|
4
|
+
* The engine answers, for one passport: "is this DPP compliant, and if not,
|
|
5
|
+
* what's missing — citing the regulation?" It composes three tiers of check:
|
|
6
|
+
*
|
|
7
|
+
* 1. STATIC — required template fields present + approved, required
|
|
8
|
+
* economic-operator parties present, filled values well-
|
|
9
|
+
* formed (pattern/enum/bounds). Applies to every category.
|
|
10
|
+
* 2. CONDITIONAL — per-category rules that only fire under a condition
|
|
11
|
+
* (e.g. battery passport required IF category is EV/LMT/
|
|
12
|
+
* industrial>2kWh; EU operator required IF manufacturer
|
|
13
|
+
* non-EU). Three categories carry binding conditionals —
|
|
14
|
+
* battery, chemicals, construction — plus one cross-cutting
|
|
15
|
+
* rule that applies to every category.
|
|
16
|
+
* 3. COVERAGE — the engine self-reports whether conditionals were
|
|
17
|
+
* evaluated or the category is static-only, so an agent
|
|
18
|
+
* NEVER reads silence as "compliant".
|
|
19
|
+
*
|
|
20
|
+
* Severity → verdict:
|
|
21
|
+
* - any `critical` → "incomplete"
|
|
22
|
+
* - else any `warning` → "compliant_with_warnings"
|
|
23
|
+
* - else → "compliant"
|
|
24
|
+
*/
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for rolling passport-field status counts up into the
|
|
3
|
+
* `fieldCounts` and `completionPercentage` denormalized on the passport row.
|
|
4
|
+
*
|
|
5
|
+
* The same derivation logic was duplicated across the field edit, batch edit,
|
|
6
|
+
* review, and agent-pipeline paths — getting it in one place means the number
|
|
7
|
+
* the UI shows is always consistent no matter which writer touched the fields
|
|
8
|
+
* last.
|
|
9
|
+
*/
|
|
10
|
+
import type { FieldStatus } from "@tracepass/dpp-types";
|
|
11
|
+
export interface FieldCounts {
|
|
12
|
+
total: number;
|
|
13
|
+
empty: number;
|
|
14
|
+
pendingReview: number;
|
|
15
|
+
approved: number;
|
|
16
|
+
flagged: number;
|
|
17
|
+
}
|
|
18
|
+
export interface PassportFieldsRollup {
|
|
19
|
+
fieldCounts: FieldCounts;
|
|
20
|
+
completionPercentage: number;
|
|
21
|
+
}
|
|
22
|
+
/** Narrow shape — we only care about the status of each field to roll counts up. */
|
|
23
|
+
export interface CountableField {
|
|
24
|
+
status: FieldStatus;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* completionPercentage is explicitly the share of approved fields, not the
|
|
28
|
+
* share of non-empty fields — a field that's pending_review or flagged is
|
|
29
|
+
* *known about* but not yet confirmed, so it doesn't count as done.
|
|
30
|
+
*/
|
|
31
|
+
export declare function derivePassportCounts(fields: Record<string, CountableField>): PassportFieldsRollup;
|
|
32
|
+
//# sourceMappingURL=counts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"counts.d.ts","sourceRoot":"","sources":["../../src/vendor/counts.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,WAAW,CAAC;IACzB,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GACrC,oBAAoB,CAatB"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for rolling passport-field status counts up into the
|
|
3
|
+
* `fieldCounts` and `completionPercentage` denormalized on the passport row.
|
|
4
|
+
*
|
|
5
|
+
* The same derivation logic was duplicated across the field edit, batch edit,
|
|
6
|
+
* review, and agent-pipeline paths — getting it in one place means the number
|
|
7
|
+
* the UI shows is always consistent no matter which writer touched the fields
|
|
8
|
+
* last.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* completionPercentage is explicitly the share of approved fields, not the
|
|
12
|
+
* share of non-empty fields — a field that's pending_review or flagged is
|
|
13
|
+
* *known about* but not yet confirmed, so it doesn't count as done.
|
|
14
|
+
*/
|
|
15
|
+
export function derivePassportCounts(fields) {
|
|
16
|
+
const values = Object.values(fields);
|
|
17
|
+
const total = values.length;
|
|
18
|
+
const fieldCounts = {
|
|
19
|
+
total,
|
|
20
|
+
empty: values.filter((f) => f.status === "empty").length,
|
|
21
|
+
pendingReview: values.filter((f) => f.status === "pending_review").length,
|
|
22
|
+
approved: values.filter((f) => f.status === "approved").length,
|
|
23
|
+
flagged: values.filter((f) => f.status === "flagged").length,
|
|
24
|
+
};
|
|
25
|
+
const completionPercentage = total > 0 ? Math.round((fieldCounts.approved / total) * 100) : 0;
|
|
26
|
+
return { fieldCounts, completionPercentage };
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=counts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"counts.js","sourceRoot":"","sources":["../../src/vendor/counts.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAsBH;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAsC;IAEtC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;IAC5B,MAAM,WAAW,GAAgB;QAC/B,KAAK;QACL,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,MAAM;QACxD,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,gBAAgB,CAAC,CAAC,MAAM;QACzE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,MAAM;QAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,MAAM;KAC7D,CAAC;IACF,MAAM,oBAAoB,GACxB,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,QAAQ,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC;AAC/C,CAAC"}
|