dcc 0.2.0 → 0.2.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.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +19 -0
  3. data/.github/workflows/release.yml +28 -0
  4. data/.gitignore +4 -0
  5. data/.rubocop_todo.yml +560 -10
  6. data/CHANGELOG.adoc +34 -1
  7. data/CONTRIBUTING.adoc +8 -1
  8. data/Gemfile +5 -7
  9. data/README.adoc +162 -7
  10. data/dcc.gemspec +8 -2
  11. data/docs/schema-versions.adoc +20 -7
  12. data/examples/build_minimal_dcc.rb +0 -0
  13. data/examples/convert_all_formats.rb +2 -1
  14. data/examples/parse_and_inspect.rb +0 -0
  15. data/examples/quantity_math_demo.rb +7 -7
  16. data/examples/validate_full.rb +1 -1
  17. data/lib/dcc/base/calibration_laboratory.rb +2 -1
  18. data/lib/dcc/base/condition.rb +5 -2
  19. data/lib/dcc/base/core_data.rb +8 -3
  20. data/lib/dcc/base/formula.rb +3 -3
  21. data/lib/dcc/base/identification.rb +6 -3
  22. data/lib/dcc/base/mathml.rb +33 -0
  23. data/lib/dcc/base/quantity.rb +2 -1
  24. data/lib/dcc/base/resp_person.rb +4 -2
  25. data/lib/dcc/base/text.rb +1 -1
  26. data/lib/dcc/base.rb +2 -1
  27. data/lib/dcc/builder/dsl.rb +1 -1
  28. data/lib/dcc/builder/session.rb +16 -14
  29. data/lib/dcc/builder.rb +3 -3
  30. data/lib/dcc/cli/cli.rb +36 -18
  31. data/lib/dcc/cli/formatters.rb +35 -7
  32. data/lib/dcc/cli.rb +1 -1
  33. data/lib/dcc/context_configuration.rb +2 -1
  34. data/lib/dcc/context_options.rb +4 -2
  35. data/lib/dcc/convert/csv.rb +27 -9
  36. data/lib/dcc/convert/html.rb +26 -110
  37. data/lib/dcc/convert/json.rb +10 -7
  38. data/lib/dcc/convert/liquid_filters.rb +58 -0
  39. data/lib/dcc/convert/result.rb +1 -1
  40. data/lib/dcc/convert/templates/report.liquid +250 -0
  41. data/lib/dcc/convert/yaml.rb +1 -1
  42. data/lib/dcc/convert.rb +1 -0
  43. data/lib/dcc/diff/change.rb +1 -1
  44. data/lib/dcc/diff/result.rb +8 -4
  45. data/lib/dcc/diff.rb +43 -11
  46. data/lib/dcc/error.rb +7 -0
  47. data/lib/dcc/extract/file.rb +3 -4
  48. data/lib/dcc/extract/ring.rb +1 -1
  49. data/lib/dcc/extract.rb +1 -1
  50. data/lib/dcc/i18n/text_lookup.rb +8 -4
  51. data/lib/dcc/i18n.rb +1 -1
  52. data/lib/dcc/inspect/report.rb +51 -14
  53. data/lib/dcc/inspect.rb +1 -1
  54. data/lib/dcc/migrate/route.rb +44 -0
  55. data/lib/dcc/migrate/v2_to_v3.rb +445 -0
  56. data/lib/dcc/migrate.rb +130 -26
  57. data/lib/dcc/plugin/base.rb +64 -0
  58. data/lib/dcc/plugin/registry.rb +7 -2
  59. data/lib/dcc/plugin.rb +6 -2
  60. data/lib/dcc/quantity_format/formatter.rb +16 -10
  61. data/lib/dcc/quantity_format.rb +1 -1
  62. data/lib/dcc/quantity_math/quantity.rb +1 -1
  63. data/lib/dcc/quantity_math/real.rb +6 -6
  64. data/lib/dcc/quantity_math.rb +1 -1
  65. data/lib/dcc/schema/version.rb +2 -2
  66. data/lib/dcc/server/app.html +282 -0
  67. data/lib/dcc/server/app.rb +171 -87
  68. data/lib/dcc/server/demo_cert.xml +244 -0
  69. data/lib/dcc/server/storage/entry.rb +1 -1
  70. data/lib/dcc/server/storage/memory.rb +3 -2
  71. data/lib/dcc/server/storage.rb +1 -1
  72. data/lib/dcc/server.rb +1 -1
  73. data/lib/dcc/si/base/complex.rb +1 -1
  74. data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
  75. data/lib/dcc/si/base/constant.rb +1 -1
  76. data/lib/dcc/si/base/coverage_interval.rb +36 -0
  77. data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
  78. data/lib/dcc/si/base/expanded_mu.rb +1 -1
  79. data/lib/dcc/si/base/expanded_unc.rb +34 -0
  80. data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
  81. data/lib/dcc/si/base/hybrid.rb +3 -2
  82. data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
  83. data/lib/dcc/si/base/real.rb +13 -3
  84. data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
  85. data/lib/dcc/si/base/si_list.rb +3 -2
  86. data/lib/dcc/si/base/standard_mu.rb +1 -1
  87. data/lib/dcc/si/base.rb +5 -2
  88. data/lib/dcc/si/v1/coverage_interval.rb +8 -0
  89. data/lib/dcc/si/v1/expanded_unc.rb +1 -16
  90. data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
  91. data/lib/dcc/si/v1.rb +5 -2
  92. data/lib/dcc/si/v2/coverage_interval.rb +8 -0
  93. data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
  94. data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
  95. data/lib/dcc/si/v2/expanded_unc.rb +8 -0
  96. data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
  97. data/lib/dcc/si/v2.rb +8 -3
  98. data/lib/dcc/signature/result.rb +2 -2
  99. data/lib/dcc/signature/signer.rb +31 -18
  100. data/lib/dcc/signature/verifier.rb +23 -21
  101. data/lib/dcc/signature.rb +1 -1
  102. data/lib/dcc/transform/result.rb +1 -1
  103. data/lib/dcc/transform/xslt.rb +22 -12
  104. data/lib/dcc/transform.rb +5 -4
  105. data/lib/dcc/type/base64_binary.rb +5 -3
  106. data/lib/dcc/type/decimal_xml_list.rb +99 -13
  107. data/lib/dcc/type/iso_country_code.rb +3 -2
  108. data/lib/dcc/type/iso_language_code.rb +3 -2
  109. data/lib/dcc/type/schema_version.rb +3 -2
  110. data/lib/dcc/type/si_unit.rb +3 -2
  111. data/lib/dcc/type_guards.rb +3 -3
  112. data/lib/dcc/v2/calibration_laboratory.rb +2 -1
  113. data/lib/dcc/v2/condition.rb +22 -1
  114. data/lib/dcc/v2/configuration.rb +1 -1
  115. data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
  116. data/lib/dcc/v2/identification.rb +14 -1
  117. data/lib/dcc/v2/mathml.rb +8 -0
  118. data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
  119. data/lib/dcc/v2/quantity.rb +4 -2
  120. data/lib/dcc/v2.rb +4 -2
  121. data/lib/dcc/v3/calibration_laboratory.rb +2 -1
  122. data/lib/dcc/v3/configuration.rb +1 -1
  123. data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
  124. data/lib/dcc/v3/mathml.rb +8 -0
  125. data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
  126. data/lib/dcc/v3/quantity.rb +4 -2
  127. data/lib/dcc/v3.rb +12 -2
  128. data/lib/dcc/validate/business_rules.rb +23 -9
  129. data/lib/dcc/validate/issue.rb +2 -1
  130. data/lib/dcc/validate/result.rb +6 -4
  131. data/lib/dcc/validate/schematron/profile.rb +73 -18
  132. data/lib/dcc/validate/schematron/rule.rb +13 -3
  133. data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
  134. data/lib/dcc/validate/schematron/rules/base.rb +1 -2
  135. data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
  136. data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
  137. data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
  138. data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
  139. data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
  140. data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
  141. data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
  142. data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
  143. data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
  144. data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
  145. data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
  146. data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
  147. data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
  148. data/lib/dcc/validate/schematron/rules.rb +28 -14
  149. data/lib/dcc/validate/schematron.rb +1 -1
  150. data/lib/dcc/validate/xsd.rb +9 -3
  151. data/lib/dcc/validate.rb +1 -1
  152. data/lib/dcc/version.rb +1 -1
  153. data/lib/dcc/versioned_parser.rb +1 -1
  154. data/lib/dcc.rb +55 -7
  155. data/sample_dcc.xml +140 -0
  156. data/web/index.html +12 -0
  157. data/web/package-lock.json +1599 -0
  158. data/web/package.json +22 -0
  159. data/web/src/App.vue +349 -0
  160. data/web/src/lib/builder.ts +171 -0
  161. data/web/src/lib/dcc.ts +338 -0
  162. data/web/src/lib/units.json +5581 -0
  163. data/web/src/main.ts +5 -0
  164. data/web/src/style.css +27 -0
  165. data/web/tsconfig.json +15 -0
  166. data/web/vite.config.ts +14 -0
  167. metadata +125 -9
@@ -0,0 +1,338 @@
1
+ export interface DccContent {
2
+ lang?: string;
3
+ text: string;
4
+ }
5
+
6
+ export interface DccQuantity {
7
+ name: string;
8
+ value?: string;
9
+ unit?: string;
10
+ uncertainty?: string;
11
+ }
12
+
13
+ export interface DccResult {
14
+ name: string;
15
+ quantities: DccQuantity[];
16
+ }
17
+
18
+ export interface DccMeasurementResult {
19
+ name: string;
20
+ results: DccResult[];
21
+ }
22
+
23
+ export interface DccSigner {
24
+ name: string;
25
+ email?: string;
26
+ mainSigner: boolean;
27
+ }
28
+
29
+ export interface DccSignature {
30
+ present: boolean;
31
+ method?: string;
32
+ digestMethod?: string;
33
+ signatureValue?: string;
34
+ certificateHash?: string;
35
+ certificateSubject?: string;
36
+ verified: boolean;
37
+ verificationResult?: boolean;
38
+ verificationError?: string;
39
+ }
40
+
41
+ export interface DccCert {
42
+ schemaVersion: string;
43
+ uniqueId: string;
44
+ country: string;
45
+ itemName: string;
46
+ labName: string;
47
+ labEmail?: string;
48
+ labAddress?: string;
49
+ model?: string;
50
+ manufacturer?: string;
51
+ coreData: { label: string; value: string }[];
52
+ signers: DccSigner[];
53
+ measurements: DccMeasurementResult[];
54
+ signature: DccSignature;
55
+ }
56
+
57
+ export interface ValidationIssue {
58
+ severity: "error" | "warning" | "info";
59
+ message: string;
60
+ line?: number;
61
+ }
62
+
63
+ export interface ValidationResult {
64
+ ok: boolean;
65
+ issues: ValidationIssue[];
66
+ }
67
+
68
+ const DCC_NS = "https://ptb.de/dcc";
69
+ const SI_NS = "https://ptb.de/si";
70
+ const DSIG_NS = "http://www.w3.org/2000/09/xmldsig#";
71
+
72
+ function localName(tag: string): string {
73
+ return tag.includes(":") ? tag.split(":").pop()! : tag;
74
+ }
75
+
76
+ function getText(el: Element | null | undefined): string {
77
+ if (!el) return "";
78
+ const contents = el.getElementsByTagNameNS(DCC_NS, "content");
79
+ if (contents.length === 0) return el.textContent?.trim() || "";
80
+ for (let i = 0; i < contents.length; i++) {
81
+ const c = contents[i];
82
+ const lang = c.getAttribute("lang") || c.getAttributeNS("http://www.w3.org/XML/1998/namespace", "lang");
83
+ if (lang === "en") return c.textContent?.trim() || "";
84
+ }
85
+ return contents[0]?.textContent?.trim() || "";
86
+ }
87
+
88
+ function getFirst(el: Element | null, localName: string, ns = DCC_NS): Element | null {
89
+ if (!el) return null;
90
+ return el.getElementsByTagNameNS(ns, localName)[0] || null;
91
+ }
92
+
93
+ function getAll(el: Element | null, localName: string, ns = DCC_NS): Element[] {
94
+ if (!el) return [];
95
+ return Array.from(el.getElementsByTagNameNS(ns, localName));
96
+ }
97
+
98
+ export function parseDcc(xml: string): DccCert {
99
+ const doc = new DOMParser().parseFromString(xml, "text/xml");
100
+ const root = doc.documentElement;
101
+ const admin = getFirst(root, "administrativeData");
102
+ const core = getFirst(admin, "coreData");
103
+ const items = getFirst(admin, "items");
104
+ const item = getFirst(items, "item");
105
+ const lab = getFirst(admin, "calibrationLaboratory");
106
+ const labContact = getFirst(lab, "contact");
107
+ const respPersons = getFirst(admin, "respPersons");
108
+ const customer = getFirst(admin, "customer");
109
+ const measurementResults = getFirst(root, "measurementResults");
110
+
111
+ const coreData: { label: string; value: string }[] = [];
112
+ if (core) {
113
+ const uid = getText(getFirst(core, "uniqueIdentifier"));
114
+ if (uid) coreData.push({ label: "Unique ID", value: uid });
115
+
116
+ const begin = getFirst(core, "beginPerformanceDate")?.textContent?.trim();
117
+ if (begin) coreData.push({ label: "Begin Date", value: begin });
118
+
119
+ const end = getFirst(core, "endPerformanceDate")?.textContent?.trim();
120
+ if (end) coreData.push({ label: "End Date", value: end });
121
+
122
+ const langs = getAll(core, "usedLangCodeISO639_1").map((e) => e.textContent?.trim() || "");
123
+ if (langs.length) coreData.push({ label: "Languages", value: langs.join(", ") });
124
+
125
+ const mandatory = getFirst(core, "mandatoryLangCodeISO639_1")?.textContent?.trim();
126
+ if (mandatory) coreData.push({ label: "Mandatory Lang", value: mandatory });
127
+ }
128
+
129
+ const signers: DccSigner[] = getAll(respPersons, "respPerson").map((rp) => {
130
+ const person = getFirst(rp, "person");
131
+ return {
132
+ name: getText(getFirst(person, "name")),
133
+ email: getFirst(person, "eMail")?.textContent?.trim() || undefined,
134
+ mainSigner: getFirst(rp, "mainSigner")?.textContent?.trim() === "true",
135
+ };
136
+ });
137
+
138
+ const measurements: DccMeasurementResult[] = getAll(measurementResults, "measurementResult").map((mr) => {
139
+ const results: DccResult[] = getAll(getFirst(mr, "results"), "result").map((r) => {
140
+ const quantities: DccQuantity[] = [];
141
+
142
+ const dataEls = getAll(r, "data");
143
+ for (const data of dataEls) {
144
+ for (const q of getAll(data, "quantity")) {
145
+ const name = getText(getFirst(q, "name"));
146
+
147
+ for (const siEl of Array.from(q.children)) {
148
+ if (siEl.namespaceURI === SI_NS) {
149
+ const value = getFirst(siEl, "value", SI_NS)?.textContent?.trim();
150
+ const unit = getFirst(siEl, "unit", SI_NS)?.textContent?.trim();
151
+ const expMu = getFirst(siEl, "expandedMU", SI_NS);
152
+ const expUnc = getFirst(siEl, "expandedUnc", SI_NS);
153
+ const muEl = expMu || expUnc;
154
+ const uncertainty = getFirst(muEl, "valueExpandedMU", SI_NS)?.textContent?.trim()
155
+ || getFirst(muEl, "uncertainty", SI_NS)?.textContent?.trim();
156
+
157
+ if (value) {
158
+ quantities.push({ name: name || "—", value, unit: unit || "", uncertainty: uncertainty || undefined });
159
+ }
160
+ }
161
+ }
162
+
163
+ if (quantities.length === 0 && name) {
164
+ quantities.push({ name });
165
+ }
166
+ }
167
+ }
168
+
169
+ return { name: getText(getFirst(r, "name")), quantities };
170
+ });
171
+
172
+ return { name: getText(getFirst(mr, "name")), results };
173
+ });
174
+
175
+ const loc = getFirst(labContact, "location");
176
+ const addrParts: string[] = [];
177
+ if (loc) {
178
+ const street = getFirst(loc, "street")?.textContent?.trim();
179
+ const no = getFirst(loc, "streetNo")?.textContent?.trim();
180
+ if (street) addrParts.push(`${street} ${no || ""}`.trim());
181
+ const pc = getFirst(loc, "postCode")?.textContent?.trim();
182
+ const city = getFirst(loc, "city")?.textContent?.trim();
183
+ if (pc) addrParts.push(`${pc} ${city || ""}`.trim());
184
+ const cc = getFirst(loc, "countryCode")?.textContent?.trim();
185
+ if (cc) addrParts.push(cc);
186
+ }
187
+
188
+ return {
189
+ schemaVersion: root.getAttribute("schemaVersion") || "unknown",
190
+ uniqueId: getText(getFirst(core, "uniqueIdentifier")),
191
+ country: getFirst(core, "countryCodeISO3166_1")?.textContent?.trim() || "",
192
+ itemName: getText(getFirst(item, "name")),
193
+ labName: getText(getFirst(labContact, "name")),
194
+ labEmail: getFirst(labContact, "eMail")?.textContent?.trim() || undefined,
195
+ labAddress: addrParts.join(", ") || undefined,
196
+ model: getFirst(item, "model")?.textContent?.trim() || undefined,
197
+ manufacturer: getText(getFirst(getFirst(item, "manufacturer"), "name")) || undefined,
198
+ coreData,
199
+ signers,
200
+ measurements,
201
+ signature: detectSignature(doc),
202
+ };
203
+ }
204
+
205
+ function detectSignature(doc: Document): DccSignature {
206
+ const sigEl = doc.getElementsByTagNameNS(DSIG_NS, "Signature")[0];
207
+ if (!sigEl) {
208
+ return { present: false, verified: false };
209
+ }
210
+
211
+ const signedInfo = sigEl.getElementsByTagNameNS(DSIG_NS, "SignedInfo")[0];
212
+ const sigMethod = signedInfo?.getElementsByTagNameNS(DSIG_NS, "SignatureMethod")[0];
213
+ const ref = signedInfo?.getElementsByTagNameNS(DSIG_NS, "Reference")[0];
214
+ const digestMethod = ref?.getElementsByTagNameNS(DSIG_NS, "DigestMethod")[0];
215
+ const sigValue = sigEl.getElementsByTagNameNS(DSIG_NS, "SignatureValue")[0];
216
+ const x509 = sigEl.getElementsByTagNameNS(DSIG_NS, "X509Certificate")[0];
217
+
218
+ let certHash: string | undefined;
219
+ if (x509?.textContent?.trim()) {
220
+ try {
221
+ const der = atob(x509.textContent.trim());
222
+ let hash = 0;
223
+ for (let i = 0; i < der.length; i++) {
224
+ hash = ((hash << 5) - hash + der.charCodeAt(i)) | 0;
225
+ }
226
+ certHash = Math.abs(hash).toString(16).padStart(8, "0").toUpperCase();
227
+ } catch {}
228
+ }
229
+
230
+ return {
231
+ present: true,
232
+ method: sigMethod?.getAttribute("Algorithm") || undefined,
233
+ digestMethod: digestMethod?.getAttribute("Algorithm") || undefined,
234
+ signatureValue: sigValue?.textContent?.trim()?.substring(0, 40) + "..." || undefined,
235
+ certificateHash: certHash,
236
+ verified: false,
237
+ };
238
+ }
239
+
240
+ export async function verifySignature(xml: string): Promise<DccSignature> {
241
+ const { SignedXml } = await import("xmldsigjs");
242
+ const doc = new DOMParser().parseFromString(xml, "text/xml");
243
+ const sigEl = doc.getElementsByTagNameNS(DSIG_NS, "Signature")[0];
244
+
245
+ if (!sigEl) {
246
+ return { present: false, verified: false };
247
+ }
248
+
249
+ const base = detectSignature(doc);
250
+
251
+ try {
252
+ const signed = new SignedXml(doc);
253
+ await signed.LoadXml(sigEl);
254
+ const ok = await signed.Verify();
255
+ return { ...base, verified: true, verificationResult: ok };
256
+ } catch (e) {
257
+ return { ...base, verified: true, verificationResult: false, verificationError: (e as Error).message };
258
+ }
259
+ }
260
+
261
+ export function validateDcc(xml: string): ValidationResult {
262
+ const issues: ValidationIssue[] = [];
263
+
264
+ let doc: Document;
265
+ try {
266
+ doc = new DOMParser().parseFromString(xml, "text/xml");
267
+ } catch (e) {
268
+ return { ok: false, issues: [{ severity: "error", message: `XML parse error: ${(e as Error).message}` }] };
269
+ }
270
+
271
+ const parseError = doc.querySelector("parsererror");
272
+ if (parseError) {
273
+ return { ok: false, issues: [{ severity: "error", message: parseError.textContent || "Malformed XML" }] };
274
+ }
275
+
276
+ const root = doc.documentElement;
277
+ if (!root || localName(root.tagName) !== "digitalCalibrationCertificate") {
278
+ issues.push({ severity: "error", message: "Root element must be <digitalCalibrationCertificate>" });
279
+ return { ok: false, issues };
280
+ }
281
+
282
+ const sv = root.getAttribute("schemaVersion");
283
+ if (!sv) {
284
+ issues.push({ severity: "warning", message: "Missing schemaVersion attribute" });
285
+ } else if (!sv.match(/^\d+\.\d+\.\d+/)) {
286
+ issues.push({ severity: "error", message: `Invalid schemaVersion format: "${sv}" (expected X.Y.Z)` });
287
+ }
288
+
289
+ const admin = getFirst(root, "administrativeData");
290
+ if (!admin) {
291
+ issues.push({ severity: "error", message: "Missing required element: <administrativeData>" });
292
+ } else {
293
+ const required = ["dccSoftware", "coreData", "items", "calibrationLaboratory", "respPersons", "customer"];
294
+ for (const name of required) {
295
+ if (!getFirst(admin, name)) {
296
+ issues.push({ severity: "error", message: `Missing required element: <${name}> in <administrativeData>` });
297
+ }
298
+ }
299
+
300
+ const core = getFirst(admin, "coreData");
301
+ if (core) {
302
+ const uid = getFirst(core, "uniqueIdentifier");
303
+ if (!uid || !uid.textContent?.trim()) {
304
+ issues.push({ severity: "error", message: "<uniqueIdentifier> is empty or missing" });
305
+ }
306
+
307
+ const country = getFirst(core, "countryCodeISO3166_1")?.textContent?.trim();
308
+ if (country && !country.match(/^[A-Z]{2}$/)) {
309
+ issues.push({ severity: "error", message: `Invalid ISO 3166-1 country code: "${country}" (expected 2 uppercase letters)` });
310
+ }
311
+
312
+ const begin = getFirst(core, "beginPerformanceDate")?.textContent?.trim();
313
+ const end = getFirst(core, "endPerformanceDate")?.textContent?.trim();
314
+ if (begin && end && begin > end) {
315
+ issues.push({ severity: "error", message: `endPerformanceDate (${end}) precedes beginPerformanceDate (${begin})` });
316
+ }
317
+ }
318
+ }
319
+
320
+ const mr = getFirst(root, "measurementResults");
321
+ if (!mr) {
322
+ issues.push({ severity: "error", message: "Missing required element: <measurementResults>" });
323
+ }
324
+
325
+ const persons = getAll(getFirst(root, "administrativeData"), "respPerson");
326
+ const mainSigners = persons.filter((p) => getFirst(p, "mainSigner")?.textContent?.trim() === "true");
327
+ if (persons.length > 0 && mainSigners.length === 0) {
328
+ issues.push({ severity: "warning", message: "No mainSigner designated among responsible persons" });
329
+ }
330
+ if (mainSigners.length > 1) {
331
+ issues.push({ severity: "warning", message: "Multiple mainSigner=true found; expected exactly one" });
332
+ }
333
+
334
+ return {
335
+ ok: issues.filter((i) => i.severity === "error").length === 0,
336
+ issues,
337
+ };
338
+ }