@euconform/core 1.4.0 → 1.6.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.
Files changed (179) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +46 -0
  3. package/dist/datasets/crows-pairs.d.ts +18 -0
  4. package/dist/datasets/crows-pairs.d.ts.map +1 -1
  5. package/dist/datasets/crows-pairs.js +58 -0
  6. package/dist/evidence/bias-comparison-markdown.d.ts +11 -0
  7. package/dist/evidence/bias-comparison-markdown.d.ts.map +1 -0
  8. package/dist/evidence/bias-comparison-markdown.js +120 -0
  9. package/dist/evidence/bias-comparison.d.ts +133 -0
  10. package/dist/evidence/bias-comparison.d.ts.map +1 -0
  11. package/dist/evidence/bias-comparison.js +274 -0
  12. package/dist/evidence/bias-statistics.d.ts +72 -0
  13. package/dist/evidence/bias-statistics.d.ts.map +1 -0
  14. package/dist/evidence/bias-statistics.js +122 -0
  15. package/dist/evidence/bias-v2-semantics.d.ts +19 -0
  16. package/dist/evidence/bias-v2-semantics.d.ts.map +1 -0
  17. package/dist/evidence/bias-v2-semantics.js +133 -0
  18. package/dist/evidence/browser.d.ts +18 -0
  19. package/dist/evidence/browser.d.ts.map +1 -0
  20. package/dist/evidence/browser.js +14 -0
  21. package/dist/evidence/bundle.d.ts +1 -0
  22. package/dist/evidence/bundle.d.ts.map +1 -1
  23. package/dist/evidence/bundle.js +2 -0
  24. package/dist/evidence/index.d.ts +9 -2
  25. package/dist/evidence/index.d.ts.map +1 -1
  26. package/dist/evidence/index.js +5 -1
  27. package/dist/evidence/output.d.ts.map +1 -1
  28. package/dist/evidence/output.js +1 -0
  29. package/dist/evidence/types.d.ts +183 -3
  30. package/dist/evidence/types.d.ts.map +1 -1
  31. package/dist/evidence/validate-bias-v2.d.ts +31 -0
  32. package/dist/evidence/validate-bias-v2.d.ts.map +1 -0
  33. package/dist/evidence/validate-bias-v2.js +40 -0
  34. package/dist/evidence/validate.d.ts +29 -2
  35. package/dist/evidence/validate.d.ts.map +1 -1
  36. package/dist/evidence/validate.js +96 -11
  37. package/dist/fairness-metrics.d.ts +15 -11
  38. package/dist/fairness-metrics.d.ts.map +1 -1
  39. package/dist/fairness-metrics.js +59 -183
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/inference/browser-inference.d.ts +6 -57
  44. package/dist/inference/browser-inference.d.ts.map +1 -1
  45. package/dist/inference/browser-inference.js +6 -219
  46. package/dist/inference/capability-detection.d.ts +9 -2
  47. package/dist/inference/capability-detection.d.ts.map +1 -1
  48. package/dist/inference/capability-detection.js +18 -16
  49. package/dist/inference/index.d.ts +15 -2
  50. package/dist/inference/index.d.ts.map +1 -1
  51. package/dist/inference/index.js +12 -3
  52. package/dist/inference/inference-factory.d.ts +6 -3
  53. package/dist/inference/inference-factory.d.ts.map +1 -1
  54. package/dist/inference/inference-factory.js +7 -26
  55. package/dist/inference/ollama-client.d.ts +29 -0
  56. package/dist/inference/ollama-client.d.ts.map +1 -1
  57. package/dist/inference/ollama-client.js +29 -0
  58. package/dist/inference/scored-log-prob-types.d.ts +64 -0
  59. package/dist/inference/scored-log-prob-types.d.ts.map +1 -0
  60. package/dist/inference/scored-log-prob-types.js +12 -0
  61. package/dist/inference/sequence-logprob.d.ts +34 -0
  62. package/dist/inference/sequence-logprob.d.ts.map +1 -0
  63. package/dist/inference/sequence-logprob.js +87 -0
  64. package/dist/inference/template-scoring.d.ts +124 -0
  65. package/dist/inference/template-scoring.d.ts.map +1 -0
  66. package/dist/inference/template-scoring.js +164 -0
  67. package/dist/inference/transformers-inference.d.ts +124 -0
  68. package/dist/inference/transformers-inference.d.ts.map +1 -0
  69. package/dist/inference/transformers-inference.js +392 -0
  70. package/dist/legal-checks/bias-metrics.d.ts +5 -7
  71. package/dist/legal-checks/bias-metrics.d.ts.map +1 -1
  72. package/dist/legal-checks/bias-metrics.js +8 -101
  73. package/dist/metrics/crows-pairs-bias.d.ts +17 -9
  74. package/dist/metrics/crows-pairs-bias.d.ts.map +1 -1
  75. package/dist/metrics/crows-pairs-bias.js +147 -59
  76. package/dist/metrics/stereotype-bias.d.ts +13 -5
  77. package/dist/metrics/stereotype-bias.d.ts.map +1 -1
  78. package/dist/metrics/stereotype-bias.js +16 -99
  79. package/dist/sbom/component-mapper.d.ts +26 -0
  80. package/dist/sbom/component-mapper.d.ts.map +1 -0
  81. package/dist/sbom/component-mapper.js +83 -0
  82. package/dist/sbom/cyclonedx-import.d.ts +34 -0
  83. package/dist/sbom/cyclonedx-import.d.ts.map +1 -0
  84. package/dist/sbom/cyclonedx-import.js +131 -0
  85. package/dist/sbom/index.d.ts +6 -0
  86. package/dist/sbom/index.d.ts.map +1 -0
  87. package/dist/sbom/index.js +4 -0
  88. package/dist/sbom/known-packages.d.ts +13 -0
  89. package/dist/sbom/known-packages.d.ts.map +1 -0
  90. package/dist/sbom/known-packages.js +129 -0
  91. package/dist/sbom/purl.d.ts +14 -0
  92. package/dist/sbom/purl.d.ts.map +1 -0
  93. package/dist/sbom/purl.js +56 -0
  94. package/dist/sbom/types.d.ts +39 -0
  95. package/dist/sbom/types.d.ts.map +1 -0
  96. package/dist/sbom/types.js +5 -0
  97. package/dist/scanner/detectors/ai-framework-detector.js +1 -1
  98. package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -1
  99. package/dist/scanner/detectors/local-inference-detector.js +1 -1
  100. package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -1
  101. package/dist/scanner/detectors/package-json-detector.js +1 -0
  102. package/dist/types.d.ts +103 -2
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/validation/index.d.ts +2 -0
  105. package/dist/validation/index.d.ts.map +1 -0
  106. package/dist/validation/index.js +1 -0
  107. package/dist/validation/schema-validator.d.ts +18 -0
  108. package/dist/validation/schema-validator.d.ts.map +1 -0
  109. package/dist/validation/schema-validator.js +105 -0
  110. package/package.json +9 -3
  111. package/scripts/seed-logprob-fixture.mjs +179 -0
  112. package/scripts/ts-extensionless-resolve-hook.mjs +26 -0
  113. package/src/datasets/crows-pairs.ts +74 -0
  114. package/src/evidence/bias-comparison-markdown.ts +162 -0
  115. package/src/evidence/bias-comparison.ts +432 -0
  116. package/src/evidence/bias-statistics.ts +180 -0
  117. package/src/evidence/bias-v2-semantics.ts +214 -0
  118. package/src/evidence/browser.ts +35 -0
  119. package/src/evidence/bundle.ts +2 -0
  120. package/src/evidence/index.ts +31 -0
  121. package/src/evidence/output.ts +1 -0
  122. package/src/evidence/types.ts +213 -3
  123. package/src/evidence/validate-bias-v2.ts +47 -0
  124. package/src/evidence/validate.ts +151 -17
  125. package/src/fairness-metrics.ts +79 -222
  126. package/src/index.ts +7 -0
  127. package/src/inference/browser-inference.ts +14 -256
  128. package/src/inference/capability-detection.ts +32 -18
  129. package/src/inference/index.ts +33 -5
  130. package/src/inference/inference-factory.ts +9 -30
  131. package/src/inference/ollama-client.ts +29 -0
  132. package/src/inference/scored-log-prob-types.ts +66 -0
  133. package/src/inference/sequence-logprob.ts +136 -0
  134. package/src/inference/template-scoring.ts +248 -0
  135. package/src/inference/transformers-inference.ts +494 -0
  136. package/src/legal-checks/bias-metrics.ts +20 -118
  137. package/src/metrics/crows-pairs-bias.ts +180 -83
  138. package/src/metrics/stereotype-bias.ts +27 -119
  139. package/src/sbom/component-mapper.ts +107 -0
  140. package/src/sbom/cyclonedx-import.ts +198 -0
  141. package/src/sbom/index.ts +12 -0
  142. package/src/sbom/known-packages.ts +142 -0
  143. package/src/sbom/purl.ts +66 -0
  144. package/src/sbom/types.ts +34 -0
  145. package/src/scanner/detectors/ai-framework-detector.ts +1 -1
  146. package/src/scanner/detectors/local-inference-detector.ts +2 -1
  147. package/src/scanner/detectors/package-json-detector.ts +1 -0
  148. package/src/types.ts +108 -2
  149. package/src/validation/index.ts +7 -0
  150. package/src/validation/schema-validator.ts +147 -0
  151. package/tests/annex-iv-report.test.ts +1 -1
  152. package/tests/capability-detection.test.ts +57 -0
  153. package/tests/crows-pairs-bias.test.ts +286 -186
  154. package/tests/custom-dataset.test.ts +80 -0
  155. package/tests/evidence/bias-artifact-v2.test.ts +135 -0
  156. package/tests/evidence/bias-artifact.test.ts +78 -0
  157. package/tests/evidence/bias-comparison.test.ts +397 -0
  158. package/tests/evidence/bias-statistics.test.ts +191 -0
  159. package/tests/evidence/external-observation.test.ts +167 -0
  160. package/tests/evidence/spec-schemas.test.ts +35 -2
  161. package/tests/evidence/validate-bias-v2.test.ts +180 -0
  162. package/tests/fairness-metrics.test.ts +77 -1
  163. package/tests/fixtures/cyclonedx-minimal.json +13 -0
  164. package/tests/fixtures/cyclonedx-mixed.json +17 -0
  165. package/tests/fixtures/cyclonedx-ml-bom.json +25 -0
  166. package/tests/inference/browser-inference-scoring.test.ts +159 -0
  167. package/tests/inference/fixtures/logprob-golden.json +69 -0
  168. package/tests/inference/gemma-template.integration.test.ts +81 -0
  169. package/tests/inference/logprob-golden.integration.test.ts +128 -0
  170. package/tests/inference/synthetic-logits.test.ts +188 -0
  171. package/tests/inference/template-scoring.test.ts +249 -0
  172. package/tests/inference/transformers-inference-template-mode.test.ts +214 -0
  173. package/tests/integration/bias-calculation.integration.test.ts +119 -599
  174. package/tests/sbom/component-mapper.test.ts +258 -0
  175. package/tests/sbom/cyclonedx-import.test.ts +329 -0
  176. package/tests/sbom/purl.test.ts +102 -0
  177. package/tests/stilled-ollama-paths.test.ts +76 -0
  178. package/tests/validation/schema-validator.test.ts +219 -0
  179. package/src/inference/transformers.d.ts +0 -62
@@ -5,6 +5,9 @@
5
5
  * Classification is done by the wizard from human input, not by the scanner.
6
6
  */
7
7
 
8
+ import type { CrowsPairsFailedPair, CrowsPairsPairResult, SequenceLogProbMethod } from "../types";
9
+ import type { BiasStatisticsV2 } from "./bias-statistics";
10
+
8
11
  // ---------------------------------------------------------------------------
9
12
  // Signal types
10
13
  // ---------------------------------------------------------------------------
@@ -232,11 +235,17 @@ export interface BomComponent {
232
235
  kind: BomComponentKind;
233
236
  name: string;
234
237
  version?: string;
235
- source: "package.json" | "lock-file" | "code" | "config" | "docs";
238
+ source: "package.json" | "lock-file" | "code" | "config" | "docs" | "sbom-import";
239
+ }
240
+
241
+ export interface AibomMetadata {
242
+ importSource: "cyclonedx" | "spdx" | "manual";
243
+ importTool?: string;
244
+ originalTimestamp?: string;
236
245
  }
237
246
 
238
247
  export interface AiBillOfMaterials {
239
- schemaVersion: "euconform.aibom.v1";
248
+ schemaVersion: "euconform.aibom.v1" | "euconform.aibom.v1.1";
240
249
  generatedAt: string;
241
250
  project: {
242
251
  name: string;
@@ -251,6 +260,7 @@ export interface AiBillOfMaterials {
251
260
  humanReviewFlow: boolean;
252
261
  incidentHandling: boolean;
253
262
  };
263
+ metadata?: AibomMetadata;
254
264
  }
255
265
 
256
266
  // ---------------------------------------------------------------------------
@@ -305,11 +315,143 @@ export interface ScanOutput {
305
315
  summaryMarkdown: string;
306
316
  }
307
317
 
318
+ // ---------------------------------------------------------------------------
319
+ // Bias report (euconform.bias.v1)
320
+ // ---------------------------------------------------------------------------
321
+
322
+ export interface BiasReport {
323
+ schemaVersion: "euconform.bias.v1";
324
+ generatedAt: string;
325
+ model: string;
326
+ dataset: string;
327
+ result: {
328
+ score: number;
329
+ method: string;
330
+ pairsAnalyzed: number;
331
+ stereotypicalPreference: number;
332
+ };
333
+ /** Absent in runs written before sampling was recorded; null fields mean the full dataset. */
334
+ sampling?: {
335
+ datasetSize: number;
336
+ maxPairs: number | null;
337
+ seed: number | null;
338
+ };
339
+ limitations?: string[];
340
+ /** Provenance of a user-supplied question catalog. Absent when a bundled dataset was used. */
341
+ catalog?: {
342
+ name: string;
343
+ version: string;
344
+ sha256: string;
345
+ pairCount: number;
346
+ };
347
+ /** Per-category aggregates over pairResults. Optional so pre-existing artifacts stay valid. */
348
+ byBiasType?: {
349
+ biasType: string;
350
+ pairsCount: number;
351
+ stereotypePreferenceCount: number;
352
+ stereotypePreferencePercent: number;
353
+ averageBiasScore: number;
354
+ }[];
355
+ /** One entry per analysed pair, enabling third-party recomputation of the score. */
356
+ pairResults?: {
357
+ pairId: number | null;
358
+ stereotype: string;
359
+ antiStereotype: string;
360
+ biasType: string | null;
361
+ stereotypeLogProb: number;
362
+ antiStereotypeLogProb: number;
363
+ biasScore: number;
364
+ biasDirection: "stereotype" | "anti-stereotype" | "neutral";
365
+ method: string;
366
+ }[];
367
+ }
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // Bias report v2 (euconform.bias.v2) — Messmotor-Sanierung Task 3.
371
+ //
372
+ // v1 is demoted to legacy (see validateBiasArtifact): its "logprobs_exact"
373
+ // method label sat on artifacts that, before Tasks 1-2, measured wall-clock
374
+ // nanoseconds rather than log-probabilities. v2 makes provenance mandatory
375
+ // and statistics self-checkable (the shared semantic validator recomputes
376
+ // them from pairResults and rejects any mismatch).
377
+ // ---------------------------------------------------------------------------
378
+
379
+ /** Mandatory measurement provenance — v2's core honesty guarantee. */
380
+ export interface BiasReportV2Provenance {
381
+ /** Single supported engine (Ollama cannot measure sentence log-probabilities). */
382
+ engine: "transformers";
383
+ modelId: string;
384
+ /** Resolved immutable commit SHA (40 hex chars), never a mutable ref like "main". */
385
+ modelRevision: string;
386
+ /** The tokenizer actually used; equals modelId unless a distinct tokenizer repo was loaded. */
387
+ tokenizerId: string;
388
+ transformersVersion: string;
389
+ /**
390
+ * Installed onnxruntime-node version. Ergebnisrelevant: a native ONNX Runtime version
391
+ * bump (1.14.0 -> 1.24.3 across the @xenova/transformers -> @huggingface/transformers
392
+ * migration) shifted sequence log-probabilities at the 1e-6 relative level even with
393
+ * byte-identical model weights and dtype (Messmotor-Sanierung Task 4). Must be resolved
394
+ * from the installed package at measurement time; an artifact that cannot resolve it is
395
+ * a loud failure, not a placeholder.
396
+ */
397
+ onnxruntimeVersion: string;
398
+ /** Numeric precision used for scoring, e.g. "fp32" or "q8" — ergebnisrelevant (Task 1: up to 11.9% deviation quantized vs fp32). */
399
+ dtype: string;
400
+ backend: "node-onnx";
401
+ templateStrategy: "none" | "fixed_template_span";
402
+ /** Present only when templateStrategy is "fixed_template_span". */
403
+ promptText?: string;
404
+ promptSha256?: string;
405
+ contextTokenHash?: string;
406
+ }
407
+
408
+ export interface BiasReportV2Sampling {
409
+ mode: "seeded_sample" | "full";
410
+ datasetSize: number;
411
+ maxPairs: number | null;
412
+ seed: number | null;
413
+ /** SHA-256 over the compact JSON of the full dataset that was sampled from. */
414
+ datasetSha256: string;
415
+ /** Pair ids in selection order — the exact reproducible sample. */
416
+ pairIds: (number | null)[];
417
+ }
418
+
419
+ export interface BiasReportV2 {
420
+ schemaVersion: "euconform.bias.v2";
421
+ generatedAt: string;
422
+ model: string;
423
+ dataset: string;
424
+ provenance: BiasReportV2Provenance;
425
+ sampling: BiasReportV2Sampling;
426
+ result: {
427
+ score: number;
428
+ method: SequenceLogProbMethod;
429
+ pairsAnalyzed: number;
430
+ };
431
+ runStatus: "complete" | "partial";
432
+ selectedCount: number;
433
+ analyzedCount: number;
434
+ failedCount: number;
435
+ failedPairs: CrowsPairsFailedPair[];
436
+ /** Self-checkable: the semantic validator recomputes this from pairResults and rejects on mismatch (tolerance 1e-9, unrounded). */
437
+ statistics: BiasStatisticsV2;
438
+ /** One entry per analysed pair — the raw values statistics/validation recompute from. */
439
+ pairResults: CrowsPairsPairResult[];
440
+ limitations?: string[];
441
+ /** Provenance of a user-supplied question catalog. Absent when a bundled dataset was used. */
442
+ catalog?: {
443
+ name: string;
444
+ version: string;
445
+ sha256: string;
446
+ pairCount: number;
447
+ };
448
+ }
449
+
308
450
  // ---------------------------------------------------------------------------
309
451
  // Bundle manifest (euconform.bundle.v1)
310
452
  // ---------------------------------------------------------------------------
311
453
 
312
- export type BundleArtifactRole = "report" | "aibom" | "ci" | "summary";
454
+ export type BundleArtifactRole = "report" | "aibom" | "ci" | "summary" | "bias";
313
455
 
314
456
  export interface BundleArtifactRef {
315
457
  role: BundleArtifactRole;
@@ -327,3 +469,71 @@ export interface ScanBundle {
327
469
  target: { name: string; rootPath: string };
328
470
  artifacts: BundleArtifactRef[];
329
471
  }
472
+
473
+ // ---------------------------------------------------------------------------
474
+ // External observations (euconform.external-observation.v1)
475
+ // ---------------------------------------------------------------------------
476
+
477
+ /**
478
+ * Provider-neutral envelope for an externally produced observation of a
479
+ * machine-readable declaration (rights, AI use, licence metadata, robots policy).
480
+ *
481
+ * It records what was visible at a point in time. It is NOT a licence, NOT an
482
+ * ownership determination, NOT proof of use and NOT permission — `result` only
483
+ * says whether a declaration was visible, and `unknown` must never be read as
484
+ * permission. The shape intentionally has no field that could express permission.
485
+ */
486
+ export interface ExternalObservation {
487
+ schemaVersion: "euconform.external-observation.v1";
488
+ /** Provider-scoped unique id, so the exact record can be cited and re-fetched. */
489
+ observationId: string;
490
+ /** When the provider observed the declaration (not when the artifact was exported). */
491
+ observedAt: string;
492
+ /** Who produced the observation. Identification only; it does not change the meaning. */
493
+ provider: {
494
+ name: string;
495
+ version: string;
496
+ endpoint?: string;
497
+ contact?: string;
498
+ };
499
+ subject: {
500
+ kind: "url" | "repository" | "dataset" | "artifact";
501
+ identifier: string;
502
+ /** When the subject content was fetched; bounds the validity of the statement. */
503
+ retrievedAt: string;
504
+ /** Optional hash of the observed content. */
505
+ contentSha256?: string;
506
+ };
507
+ observation: {
508
+ kind:
509
+ | "rights-declaration"
510
+ | "ai-use-declaration"
511
+ | "license-metadata"
512
+ | "robots-policy"
513
+ | "terms-of-service";
514
+ /** Query vocabulary of the provider (e.g. ai-input, ai-train). Free-form on purpose. */
515
+ intendedUse: string[];
516
+ /**
517
+ * `declared` = a supported machine-readable declaration was visible — NOT that
518
+ * the use is permitted. `unknown` must never be converted into permission.
519
+ */
520
+ result: "declared" | "not-declared" | "unknown" | "not-applicable";
521
+ /** Present exactly when `result` is `declared`, so the claim stays checkable. */
522
+ declaration?: {
523
+ text: string;
524
+ sha256: string;
525
+ url?: string;
526
+ license?: string;
527
+ };
528
+ };
529
+ /** Mandatory coverage limits; an observation without stated limits is not reviewable. */
530
+ limitations: string[];
531
+ /** Optional integrity proof. Unsigned observations are valid but must be treated as unverified. */
532
+ signature?: {
533
+ algorithm: string;
534
+ keyId: string;
535
+ value: string;
536
+ signedPayloadSha256: string;
537
+ verifyUrl?: string;
538
+ };
539
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The ONE validator entrypoint for `euconform.bias.v2` artifacts
3
+ * (Messmotor-Sanierung Task 3). Both `euconform validate` and `euconform verify`
4
+ * call this — before this task they used different validators (AJV-only vs. the
5
+ * lenient hand-rolled `validateBiasReport`) that could disagree on the same file.
6
+ *
7
+ * Validation has two layers:
8
+ * 1. Schema shape — delegated to the AJV schema (`bias-v2.schema.json` via
9
+ * `packages/core/src/validation/schema-validator.ts`), so there is exactly
10
+ * one source of truth for "what fields must exist". Node-only (reads the
11
+ * schema from disk); the browser calls `assertBiasV2Semantics` from
12
+ * `@euconform/core/evidence/browser` for layer 2.
13
+ * 2. Semantic recomputation — every statistic, and the partial-run invariants,
14
+ * are recomputed from the raw `pairResults` and compared against the stored
15
+ * values with an absolute tolerance of 1e-9 (ambiguity resolution #2:
16
+ * statistics are stored UNROUNDED; rounding only happens in the Markdown
17
+ * report, so a tolerance far below any human-rounding step is meaningful).
18
+ * A mismatch means the artifact's numbers do not follow from its own raw
19
+ * data — e.g. a manipulated Wilson bound — and the artifact is rejected
20
+ * with a message naming exactly which field disagreed.
21
+ * This layer lives in `bias-v2-semantics.ts` so it can also run in the browser.
22
+ */
23
+
24
+ import { validate as validateAgainstSchema } from "../validation/schema-validator";
25
+ import { assertBiasV2Semantics } from "./bias-v2-semantics";
26
+ import type { BiasReportV2 } from "./types";
27
+
28
+ /**
29
+ * `verify --strict` rejects a partial run outright (ambiguity resolution #3):
30
+ * a diagnostic run is, by definition, not the complete measurement a strict
31
+ * check demands. Non-strict callers get this back as a plain boolean instead.
32
+ */
33
+ export function isPartialRun(report: BiasReportV2): boolean {
34
+ return report.runStatus === "partial";
35
+ }
36
+
37
+ export function validateBiasReportV2(data: unknown): BiasReportV2 {
38
+ const schemaResult = validateAgainstSchema(data);
39
+ if (!schemaResult.valid) {
40
+ const messages = schemaResult.errors.map((e) => `${e.path || "/"} ${e.message}`).join("; ");
41
+ throw new Error(
42
+ `Invalid euconform.bias.v2 artifact: ${messages || "schema validation failed"}`
43
+ );
44
+ }
45
+
46
+ return assertBiasV2Semantics(data);
47
+ }
@@ -1,4 +1,27 @@
1
- import type { AiBillOfMaterials, CiReport, ScanBundle, ScanReport } from "./types";
1
+ import type {
2
+ AiBillOfMaterials,
3
+ BiasReport,
4
+ BiasReportV2,
5
+ CiReport,
6
+ ExternalObservation,
7
+ ScanBundle,
8
+ ScanReport,
9
+ } from "./types";
10
+ import { validateBiasReportV2 } from "./validate-bias-v2";
11
+
12
+ const V1_BIAS_LEGACY_WARNING =
13
+ "This artifact uses euconform.bias.v1: the method label 'logprobs_exact' on v1 bias " +
14
+ "artifacts is unreliable (Messmotor-Sanierung Task 1/2 found the measurement path it " +
15
+ "described could silently fall back to wall-clock nanoseconds instead of real " +
16
+ "log-probabilities). Re-measure with `euconform bias --engine transformers` to get a " +
17
+ "euconform.bias.v2 artifact.";
18
+
19
+ /** Result of validating any bias artifact, v1 (legacy) or v2. */
20
+ export interface BiasValidationResult {
21
+ legacy: boolean;
22
+ warning?: string;
23
+ document: BiasReport | BiasReportV2;
24
+ }
2
25
 
3
26
  function assertObject(data: unknown, label: string): Record<string, unknown> {
4
27
  if (!data || typeof data !== "object" || Array.isArray(data)) {
@@ -7,10 +30,16 @@ function assertObject(data: unknown, label: string): Record<string, unknown> {
7
30
  return data as Record<string, unknown>;
8
31
  }
9
32
 
10
- function requireSchemaVersion(obj: Record<string, unknown>, expected: string, label: string): void {
11
- if (obj.schemaVersion !== expected) {
33
+ function requireSchemaVersion(
34
+ obj: Record<string, unknown>,
35
+ expected: string | string[],
36
+ label: string
37
+ ): void {
38
+ const allowed = Array.isArray(expected) ? expected : [expected];
39
+ if (!allowed.includes(obj.schemaVersion as string)) {
40
+ const quoted = allowed.map((v) => `"${v}"`).join(" or ");
12
41
  throw new Error(
13
- `Invalid ${label} schema version: expected "${expected}", got "${String(obj.schemaVersion)}"`
42
+ `Invalid ${label} schema version: expected ${quoted}, got "${String(obj.schemaVersion)}"`
14
43
  );
15
44
  }
16
45
  }
@@ -102,7 +131,7 @@ export function validateScanReport(data: unknown): ScanReport {
102
131
 
103
132
  export function validateAiBillOfMaterials(data: unknown): AiBillOfMaterials {
104
133
  const obj = assertObject(data, "AIBOM");
105
- requireSchemaVersion(obj, "euconform.aibom.v1", "AIBOM");
134
+ requireSchemaVersion(obj, ["euconform.aibom.v1", "euconform.aibom.v1.1"], "AIBOM");
106
135
  requireField(obj, "generatedAt", "string", "AIBOM");
107
136
 
108
137
  const project = requireField(obj, "project", "object", "AIBOM") as Record<string, unknown>;
@@ -149,7 +178,7 @@ export function validateCiReport(data: unknown): CiReport {
149
178
  return data as CiReport;
150
179
  }
151
180
 
152
- const VALID_BUNDLE_ROLES = new Set(["report", "aibom", "ci", "summary"]);
181
+ const VALID_BUNDLE_ROLES = new Set(["report", "aibom", "ci", "summary", "bias"]);
153
182
  const SHA256_HEX = /^[a-f0-9]{64}$/;
154
183
 
155
184
  interface BundleArtifactFields {
@@ -207,13 +236,20 @@ function readBundleArtifactFields(
207
236
  };
208
237
  }
209
238
 
239
+ /**
240
+ * Bundle decoupling (Messmotor-Sanierung Task 3): artifact schema versions are
241
+ * independent of the bundle's own schema version. A `euconform.bundle.v1`
242
+ * manifest may reference a `euconform.bias.v2` artifact — the bundle's job is
243
+ * to bind artifacts by hash and role, not to pin their versions. There is
244
+ * therefore no "same major as the bundle" check here; per-artifact schema
245
+ * shape is the responsibility of that artifact's own validator.
246
+ */
210
247
  function assertBundleArtifactIntegrity(
211
248
  fields: BundleArtifactFields,
212
249
  seenRoles: Set<string>,
213
- seenFiles: Set<string>,
214
- bundleMajor: string | null
250
+ seenFiles: Set<string>
215
251
  ): void {
216
- const { role, fileName, sha256, declaredSchemaVersion } = fields;
252
+ const { role, fileName, sha256 } = fields;
217
253
 
218
254
  if (!VALID_BUNDLE_ROLES.has(role)) {
219
255
  throw new Error(`Invalid bundle artifact: unsupported role '${role}'`);
@@ -227,11 +263,6 @@ function assertBundleArtifactIntegrity(
227
263
  if (seenFiles.has(fileName)) {
228
264
  throw new Error(`Invalid bundle artifact: duplicate fileName '${fileName}'`);
229
265
  }
230
- if (declaredSchemaVersion && bundleMajor && majorVersion(declaredSchemaVersion) !== bundleMajor) {
231
- throw new Error(
232
- `Invalid bundle artifact: '${fileName}' uses schemaVersion '${declaredSchemaVersion}' with a different major than the bundle`
233
- );
234
- }
235
266
  }
236
267
 
237
268
  function validateBundleHeader(obj: Record<string, unknown>): void {
@@ -247,6 +278,107 @@ function validateBundleHeader(obj: Record<string, unknown>): void {
247
278
  requireField(target, "rootPath", "string", "bundle", "target.rootPath");
248
279
  }
249
280
 
281
+ export function validateBiasReport(data: unknown): BiasReport {
282
+ const obj = assertObject(data, "bias report");
283
+ requireSchemaVersion(obj, "euconform.bias.v1", "bias report");
284
+ requireField(obj, "generatedAt", "string", "bias report");
285
+ requireField(obj, "model", "string", "bias report");
286
+ requireField(obj, "dataset", "string", "bias report");
287
+
288
+ const result = requireField(obj, "result", "object", "bias report") as Record<string, unknown>;
289
+ requireField(result, "score", "number", "bias report", "result.score");
290
+ requireField(result, "method", "string", "bias report", "result.method");
291
+ requireField(result, "pairsAnalyzed", "number", "bias report", "result.pairsAnalyzed");
292
+ requireField(
293
+ result,
294
+ "stereotypicalPreference",
295
+ "number",
296
+ "bias report",
297
+ "result.stereotypicalPreference"
298
+ );
299
+
300
+ return data as BiasReport;
301
+ }
302
+
303
+ /**
304
+ * The ONE entry point for bias artifacts, dispatching on schemaVersion.
305
+ * `validate` (CLI) and `verify` both call this instead of picking their own
306
+ * validator, so they can no longer disagree on the same file.
307
+ *
308
+ * v1 keeps its existing (lenient) validation behaviour EXACTLY as before —
309
+ * `validateBiasReport` is untouched — but is now flagged `legacy: true` with a
310
+ * warning naming the incident. v2 is delegated to the shared semantic
311
+ * validator (`validate-bias-v2.ts`), which also recomputes statistics.
312
+ */
313
+ export function validateBiasArtifact(data: unknown): BiasValidationResult {
314
+ const obj = assertObject(data, "bias report");
315
+ if (obj.schemaVersion === "euconform.bias.v2") {
316
+ return { legacy: false, document: validateBiasReportV2(data) };
317
+ }
318
+ return {
319
+ legacy: true,
320
+ warning: V1_BIAS_LEGACY_WARNING,
321
+ document: validateBiasReport(data),
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Shape check for `euconform.external-observation.v1`. The schema is the source
327
+ * of truth for shape (`euconform validate` delegates to it); this function is the
328
+ * programmatic entrypoint, mirroring the other document validators.
329
+ *
330
+ * It deliberately performs no interpretation: whether a declaration exists is
331
+ * data, and nothing here may turn `unknown` into permission.
332
+ */
333
+ export function validateExternalObservation(data: unknown): ExternalObservation {
334
+ const label = "external observation";
335
+ const obj = assertObject(data, label);
336
+ requireSchemaVersion(obj, "euconform.external-observation.v1", label);
337
+ requireField(obj, "observationId", "string", label);
338
+ requireField(obj, "observedAt", "string", label);
339
+
340
+ const provider = requireField(obj, "provider", "object", label) as Record<string, unknown>;
341
+ requireField(provider, "name", "string", label, "provider.name");
342
+ requireField(provider, "version", "string", label, "provider.version");
343
+
344
+ const subject = requireField(obj, "subject", "object", label) as Record<string, unknown>;
345
+ requireField(subject, "kind", "string", label, "subject.kind");
346
+ requireField(subject, "identifier", "string", label, "subject.identifier");
347
+ requireField(subject, "retrievedAt", "string", label, "subject.retrievedAt");
348
+
349
+ const observation = requireField(obj, "observation", "object", label) as Record<string, unknown>;
350
+ requireField(observation, "kind", "string", label, "observation.kind");
351
+ requireStringArray(observation, "intendedUse", label, "observation.intendedUse");
352
+ const result = requireField(
353
+ observation,
354
+ "result",
355
+ "string",
356
+ label,
357
+ "observation.result"
358
+ ) as string;
359
+
360
+ if (result === "declared") {
361
+ const declaration = requireField(
362
+ observation,
363
+ "declaration",
364
+ "object",
365
+ label,
366
+ "observation.declaration"
367
+ ) as Record<string, unknown>;
368
+ requireField(declaration, "text", "string", label, "observation.declaration.text");
369
+ requireField(declaration, "sha256", "string", label, "observation.declaration.sha256");
370
+ }
371
+
372
+ const limitations = requireStringArray(obj, "limitations", label);
373
+ if (limitations.length === 0) {
374
+ throw new Error(
375
+ `Invalid ${label}: 'limitations' must name at least one coverage limit — an observation without stated limits is not reviewable`
376
+ );
377
+ }
378
+
379
+ return data as ExternalObservation;
380
+ }
381
+
250
382
  export function validateScanBundle(data: unknown): ScanBundle {
251
383
  const obj = assertObject(data, "bundle");
252
384
  validateBundleHeader(obj);
@@ -258,13 +390,12 @@ export function validateScanBundle(data: unknown): ScanBundle {
258
390
 
259
391
  const seenRoles = new Set<string>();
260
392
  const seenFiles = new Set<string>();
261
- const bundleMajor = majorVersion("euconform.bundle.v1");
262
393
  let hasRequiredReport = false;
263
394
 
264
395
  for (let index = 0; index < artifacts.length; index++) {
265
396
  const raw = assertObject(artifacts[index], "bundle artifact");
266
397
  const fields = readBundleArtifactFields(raw, index);
267
- assertBundleArtifactIntegrity(fields, seenRoles, seenFiles, bundleMajor);
398
+ assertBundleArtifactIntegrity(fields, seenRoles, seenFiles);
268
399
 
269
400
  seenRoles.add(fields.role);
270
401
  seenFiles.add(fields.fileName);
@@ -284,16 +415,19 @@ export function validateScanBundle(data: unknown): ScanBundle {
284
415
  export function validateEcefJsonDocument(
285
416
  schemaVersion: string,
286
417
  data: unknown
287
- ): ScanReport | AiBillOfMaterials | CiReport | ScanBundle {
418
+ ): ScanReport | AiBillOfMaterials | CiReport | ScanBundle | ExternalObservation {
288
419
  switch (schemaVersion) {
289
420
  case "euconform.report.v1":
290
421
  return validateScanReport(data);
291
422
  case "euconform.aibom.v1":
423
+ case "euconform.aibom.v1.1":
292
424
  return validateAiBillOfMaterials(data);
293
425
  case "euconform.ci.v1":
294
426
  return validateCiReport(data);
295
427
  case "euconform.bundle.v1":
296
428
  return validateScanBundle(data);
429
+ case "euconform.external-observation.v1":
430
+ return validateExternalObservation(data);
297
431
  default:
298
432
  throw new Error(`Unsupported EuConform Evidence Format schemaVersion '${schemaVersion}'`);
299
433
  }