@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
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Pure statistics for the `euconform.bias.v2` evidence format
3
+ * (Messmotor-Sanierung Task 3).
4
+ *
5
+ * These functions are used both by the producer (packages/cli bias command,
6
+ * via `buildBiasStatistics`) and by the shared semantic validator
7
+ * (`validate-bias-v2.ts`), which recomputes them from the raw `pairResults`
8
+ * and compares against the stored `statistics` block. Both sides MUST use the
9
+ * same `WILSON_Z95` constant — a mismatch would make every honestly-measured
10
+ * artifact fail its own validation.
11
+ *
12
+ * All values here are UNROUNDED. Rounding for human display happens only in
13
+ * the Markdown report, never in the artifact.
14
+ */
15
+
16
+ import type { CrowsPairsPairResult } from "../types";
17
+
18
+ /** z for a 95% Wilson score interval (two-sided normal quantile at 0.975). */
19
+ export const WILSON_Z95 = 1.959963984540054;
20
+
21
+ export interface WilsonInterval {
22
+ lower: number;
23
+ upper: number;
24
+ }
25
+
26
+ export interface Quartiles {
27
+ min: number;
28
+ max: number;
29
+ median: number;
30
+ q1: number;
31
+ q3: number;
32
+ }
33
+
34
+ /**
35
+ * Wilson score interval for a binomial proportion (Wilson, 1927).
36
+ *
37
+ * p̂ = successes / n
38
+ * center = (p̂ + z²/(2n)) / (1 + z²/n)
39
+ * margin = z · sqrt(p̂(1-p̂)/n + z²/(4n²)) / (1 + z²/n)
40
+ *
41
+ * Returns null when n === 0 (the proportion, and therefore the interval, is
42
+ * undefined — e.g. the equal-token-pairs subset can legitimately be empty).
43
+ */
44
+ export function wilson95(
45
+ successes: number,
46
+ n: number,
47
+ z: number = WILSON_Z95
48
+ ): WilsonInterval | null {
49
+ if (n === 0) return null;
50
+ if (successes < 0 || successes > n) {
51
+ throw new Error(`wilson95: successes (${successes}) must be within [0, n=${n}]`);
52
+ }
53
+
54
+ const pHat = successes / n;
55
+ const z2 = z * z;
56
+ const denominator = 1 + z2 / n;
57
+ const center = (pHat + z2 / (2 * n)) / denominator;
58
+ const margin = (z * Math.sqrt((pHat * (1 - pHat)) / n + z2 / (4 * n * n))) / denominator;
59
+
60
+ return { lower: center - margin, upper: center + margin };
61
+ }
62
+
63
+ /**
64
+ * Quantile type 7 (R default; numpy's 'linear' interpolation) — linear
65
+ * interpolation between order statistics, 0-indexed:
66
+ *
67
+ * h = (n - 1) * p
68
+ * value = v[floor(h)] + (h - floor(h)) * (v[floor(h) + 1] - v[floor(h)])
69
+ *
70
+ * Returns null for an empty input (undefined distribution).
71
+ */
72
+ function quantileType7(sorted: number[], p: number): number {
73
+ const n = sorted.length;
74
+ if (n === 1) return sorted[0] as number;
75
+ const h = (n - 1) * p;
76
+ const lowerIndex = Math.floor(h);
77
+ const frac = h - lowerIndex;
78
+ const lower = sorted[lowerIndex] as number;
79
+ const upper = sorted[Math.min(lowerIndex + 1, n - 1)] as number;
80
+ return lower + frac * (upper - lower);
81
+ }
82
+
83
+ export function quartilesType7(values: number[]): Quartiles | null {
84
+ if (values.length === 0) return null;
85
+ const sorted = [...values].sort((a, b) => a - b);
86
+ return {
87
+ min: sorted[0] as number,
88
+ max: sorted[sorted.length - 1] as number,
89
+ q1: quantileType7(sorted, 0.25),
90
+ median: quantileType7(sorted, 0.5),
91
+ q3: quantileType7(sorted, 0.75),
92
+ };
93
+ }
94
+
95
+ export interface CategoryPreferenceStatistics {
96
+ biasType: string;
97
+ analyzedCount: number;
98
+ preferenceCount: number;
99
+ tiesCount: number;
100
+ preferencePercent: number;
101
+ preferenceWilson95: WilsonInterval | null;
102
+ }
103
+
104
+ export interface BiasStatisticsV2 {
105
+ analyzedCount: number;
106
+ /** Pairs where biasScore > 0 (stereotype preferred). Denominator: analyzedCount. */
107
+ preferenceCount: number;
108
+ /** Pairs where biasScore === 0, reported separately, not folded into either side. */
109
+ tiesCount: number;
110
+ preferencePercent: number;
111
+ preferenceWilson95: WilsonInterval | null;
112
+ byBiasType: CategoryPreferenceStatistics[];
113
+ /** Distribution of biasScore (stereotype - anti-stereotype) across analysed pairs. */
114
+ pairDiffQuartiles: Quartiles | null;
115
+ /** Confounder control: pairs whose stereotype/anti-stereotype token counts are equal. */
116
+ equalTokenPairsCount: number;
117
+ equalTokenPreferenceCount: number;
118
+ equalTokenPreferencePercent: number | null;
119
+ equalTokenPreferenceWilson95: WilsonInterval | null;
120
+ }
121
+
122
+ function preferenceStatsOf(pairs: CrowsPairsPairResult[]): {
123
+ analyzedCount: number;
124
+ preferenceCount: number;
125
+ tiesCount: number;
126
+ preferencePercent: number;
127
+ preferenceWilson95: WilsonInterval | null;
128
+ } {
129
+ const analyzedCount = pairs.length;
130
+ const preferenceCount = pairs.filter((p) => p.biasScore > 0).length;
131
+ const tiesCount = pairs.filter((p) => p.biasScore === 0).length;
132
+ const preferencePercent = analyzedCount === 0 ? 0 : (preferenceCount / analyzedCount) * 100;
133
+ return {
134
+ analyzedCount,
135
+ preferenceCount,
136
+ tiesCount,
137
+ preferencePercent,
138
+ preferenceWilson95: wilson95(preferenceCount, analyzedCount),
139
+ };
140
+ }
141
+
142
+ function byBiasTypeStatistics(pairResults: CrowsPairsPairResult[]): CategoryPreferenceStatistics[] {
143
+ const groups = new Map<string, CrowsPairsPairResult[]>();
144
+ for (const pair of pairResults) {
145
+ const key = pair.biasType ?? "unspecified";
146
+ const bucket = groups.get(key);
147
+ if (bucket) bucket.push(pair);
148
+ else groups.set(key, [pair]);
149
+ }
150
+ return [...groups.entries()].map(([biasType, pairs]) => ({
151
+ biasType,
152
+ ...preferenceStatsOf(pairs),
153
+ }));
154
+ }
155
+
156
+ /**
157
+ * Builds the full v2 statistics block from raw per-pair measurements. Pure —
158
+ * no I/O, no rounding. This is the SAME function the semantic validator calls
159
+ * to recompute and cross-check a stored artifact's `statistics` block.
160
+ */
161
+ export function buildBiasStatistics(pairResults: CrowsPairsPairResult[]): BiasStatisticsV2 {
162
+ const overall = preferenceStatsOf(pairResults);
163
+ const equalTokenPairs = pairResults.filter((p) => p.tokenCountDelta === 0);
164
+ const equalTokenStats = preferenceStatsOf(equalTokenPairs);
165
+
166
+ return {
167
+ analyzedCount: overall.analyzedCount,
168
+ preferenceCount: overall.preferenceCount,
169
+ tiesCount: overall.tiesCount,
170
+ preferencePercent: overall.preferencePercent,
171
+ preferenceWilson95: overall.preferenceWilson95,
172
+ byBiasType: byBiasTypeStatistics(pairResults),
173
+ pairDiffQuartiles: quartilesType7(pairResults.map((p) => p.biasScore)),
174
+ equalTokenPairsCount: equalTokenPairs.length,
175
+ equalTokenPreferenceCount: equalTokenStats.preferenceCount,
176
+ equalTokenPreferencePercent:
177
+ equalTokenPairs.length === 0 ? null : equalTokenStats.preferencePercent,
178
+ equalTokenPreferenceWilson95: equalTokenStats.preferenceWilson95,
179
+ };
180
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Pure semantic checks for `euconform.bias.v2` artifacts (no Node builtins).
3
+ *
4
+ * Split out of `validate-bias-v2.ts` so the exact same recomputation can run in
5
+ * the browser (`@euconform/core/evidence/browser`). The schema-shape check stays
6
+ * Node-side because it reads the published JSON schema from disk; the semantic
7
+ * layer — "do the stored numbers follow from the raw pair results?" — must never
8
+ * have two implementations.
9
+ */
10
+
11
+ import type { CrowsPairsPairResult } from "../types";
12
+ import { type BiasStatisticsV2, type WilsonInterval, buildBiasStatistics } from "./bias-statistics";
13
+ import type { BiasReportV2 } from "./types";
14
+
15
+ const TOLERANCE = 1e-9;
16
+
17
+ function closeEnough(a: number, b: number): boolean {
18
+ return Math.abs(a - b) <= TOLERANCE;
19
+ }
20
+
21
+ function fail(message: string): never {
22
+ throw new Error(`Invalid euconform.bias.v2 artifact: ${message}`);
23
+ }
24
+
25
+ function assertNumberMatches(label: string, expected: number, actual: number): void {
26
+ if (!closeEnough(expected, actual)) {
27
+ fail(
28
+ `${label} does not match recomputation from pairResults: stored ${actual}, recomputed ${expected}`
29
+ );
30
+ }
31
+ }
32
+
33
+ function assertWilsonMatches(
34
+ label: string,
35
+ expected: WilsonInterval | null,
36
+ actual: WilsonInterval | null
37
+ ): void {
38
+ if (expected === null || actual === null) {
39
+ if (expected !== actual) {
40
+ fail(
41
+ `${label} should be ${expected === null ? "null" : "a Wilson interval"}, got ${actual === null ? "null" : JSON.stringify(actual)}`
42
+ );
43
+ }
44
+ return;
45
+ }
46
+ assertNumberMatches(`${label}.lower`, expected.lower, actual.lower);
47
+ assertNumberMatches(`${label}.upper`, expected.upper, actual.upper);
48
+ }
49
+
50
+ function assertQuartilesMatch(
51
+ expected: BiasStatisticsV2["pairDiffQuartiles"],
52
+ actual: BiasStatisticsV2["pairDiffQuartiles"]
53
+ ): void {
54
+ if (expected === null || actual === null) {
55
+ if (expected !== actual) {
56
+ fail(
57
+ `statistics.pairDiffQuartiles should be ${expected === null ? "null" : "present"}, got ${actual === null ? "null" : "present"}`
58
+ );
59
+ }
60
+ return;
61
+ }
62
+ for (const key of ["min", "max", "median", "q1", "q3"] as const) {
63
+ assertNumberMatches(`statistics.pairDiffQuartiles.${key}`, expected[key], actual[key]);
64
+ }
65
+ }
66
+
67
+ function assertCategoryStatisticsMatch(
68
+ expected: BiasStatisticsV2["byBiasType"],
69
+ actual: BiasStatisticsV2["byBiasType"]
70
+ ): void {
71
+ if (expected.length !== actual.length) {
72
+ fail(
73
+ `statistics.byBiasType has ${actual.length} categories, recomputation from pairResults yields ${expected.length}`
74
+ );
75
+ }
76
+ const actualByType = new Map(actual.map((c) => [c.biasType, c]));
77
+ for (const expectedCategory of expected) {
78
+ const actualCategory = actualByType.get(expectedCategory.biasType);
79
+ if (!actualCategory) {
80
+ fail(`statistics.byBiasType is missing category '${expectedCategory.biasType}'`);
81
+ }
82
+ const label = `statistics.byBiasType['${expectedCategory.biasType}']`;
83
+ if (expectedCategory.analyzedCount !== actualCategory.analyzedCount) {
84
+ fail(
85
+ `${label}.analyzedCount: stored ${actualCategory.analyzedCount}, recomputed ${expectedCategory.analyzedCount}`
86
+ );
87
+ }
88
+ if (expectedCategory.preferenceCount !== actualCategory.preferenceCount) {
89
+ fail(
90
+ `${label}.preferenceCount: stored ${actualCategory.preferenceCount}, recomputed ${expectedCategory.preferenceCount}`
91
+ );
92
+ }
93
+ if (expectedCategory.tiesCount !== actualCategory.tiesCount) {
94
+ fail(
95
+ `${label}.tiesCount: stored ${actualCategory.tiesCount}, recomputed ${expectedCategory.tiesCount}`
96
+ );
97
+ }
98
+ assertNumberMatches(
99
+ `${label}.preferencePercent`,
100
+ expectedCategory.preferencePercent,
101
+ actualCategory.preferencePercent
102
+ );
103
+ assertWilsonMatches(
104
+ `${label}.preferenceWilson95`,
105
+ expectedCategory.preferenceWilson95,
106
+ actualCategory.preferenceWilson95
107
+ );
108
+ }
109
+ }
110
+
111
+ function assertStatisticsMatch(expected: BiasStatisticsV2, actual: BiasStatisticsV2): void {
112
+ if (expected.analyzedCount !== actual.analyzedCount) {
113
+ fail(
114
+ `statistics.analyzedCount: stored ${actual.analyzedCount}, recomputed ${expected.analyzedCount}`
115
+ );
116
+ }
117
+ if (expected.preferenceCount !== actual.preferenceCount) {
118
+ fail(
119
+ `statistics.preferenceCount: stored ${actual.preferenceCount}, recomputed ${expected.preferenceCount}`
120
+ );
121
+ }
122
+ if (expected.tiesCount !== actual.tiesCount) {
123
+ fail(`statistics.tiesCount: stored ${actual.tiesCount}, recomputed ${expected.tiesCount}`);
124
+ }
125
+ assertNumberMatches(
126
+ "statistics.preferencePercent",
127
+ expected.preferencePercent,
128
+ actual.preferencePercent
129
+ );
130
+ assertWilsonMatches(
131
+ "statistics.preferenceWilson95",
132
+ expected.preferenceWilson95,
133
+ actual.preferenceWilson95
134
+ );
135
+ assertCategoryStatisticsMatch(expected.byBiasType, actual.byBiasType);
136
+ assertQuartilesMatch(expected.pairDiffQuartiles, actual.pairDiffQuartiles);
137
+ if (expected.equalTokenPairsCount !== actual.equalTokenPairsCount) {
138
+ fail(
139
+ `statistics.equalTokenPairsCount: stored ${actual.equalTokenPairsCount}, recomputed ${expected.equalTokenPairsCount}`
140
+ );
141
+ }
142
+ if (expected.equalTokenPreferenceCount !== actual.equalTokenPreferenceCount) {
143
+ fail(
144
+ `statistics.equalTokenPreferenceCount: stored ${actual.equalTokenPreferenceCount}, recomputed ${expected.equalTokenPreferenceCount}`
145
+ );
146
+ }
147
+ if (
148
+ expected.equalTokenPreferencePercent === null ||
149
+ actual.equalTokenPreferencePercent === null
150
+ ) {
151
+ if (expected.equalTokenPreferencePercent !== actual.equalTokenPreferencePercent) {
152
+ fail(
153
+ "statistics.equalTokenPreferencePercent must be null exactly when equalTokenPairsCount is 0 (recomputation disagrees)"
154
+ );
155
+ }
156
+ } else {
157
+ assertNumberMatches(
158
+ "statistics.equalTokenPreferencePercent",
159
+ expected.equalTokenPreferencePercent,
160
+ actual.equalTokenPreferencePercent
161
+ );
162
+ }
163
+ assertWilsonMatches(
164
+ "statistics.equalTokenPreferenceWilson95",
165
+ expected.equalTokenPreferenceWilson95,
166
+ actual.equalTokenPreferenceWilson95
167
+ );
168
+ }
169
+
170
+ function assertPartialInvariants(report: BiasReportV2): void {
171
+ if (report.selectedCount !== report.analyzedCount + report.failedCount) {
172
+ fail(
173
+ `selectedCount (${report.selectedCount}) must equal analyzedCount (${report.analyzedCount}) + failedCount (${report.failedCount})`
174
+ );
175
+ }
176
+ if (report.failedPairs.length !== report.failedCount) {
177
+ fail(
178
+ `failedPairs.length (${report.failedPairs.length}) must equal failedCount (${report.failedCount})`
179
+ );
180
+ }
181
+ if (report.runStatus === "complete" && report.failedCount !== 0) {
182
+ fail(`runStatus is 'complete' but failedCount is ${report.failedCount}`);
183
+ }
184
+ if (report.runStatus === "partial" && report.failedCount === 0) {
185
+ fail("runStatus is 'partial' but failedCount is 0 (no evidence of a partial run)");
186
+ }
187
+ if (report.result.pairsAnalyzed !== report.analyzedCount) {
188
+ fail(
189
+ `result.pairsAnalyzed (${report.result.pairsAnalyzed}) must equal analyzedCount (${report.analyzedCount})`
190
+ );
191
+ }
192
+ if (report.pairResults.length !== report.analyzedCount) {
193
+ fail(
194
+ `pairResults.length (${report.pairResults.length}) must equal analyzedCount (${report.analyzedCount})`
195
+ );
196
+ }
197
+ }
198
+
199
+ export function recomputedBiasScore(pairResults: CrowsPairsPairResult[]): number {
200
+ return pairResults.reduce((sum, p) => sum + p.biasScore, 0) / pairResults.length;
201
+ }
202
+
203
+ /**
204
+ * Recomputes every stored statistic from `pairResults` and rejects any artifact
205
+ * whose stored values do not follow from its own raw data (tolerance 1e-9, i.e.
206
+ * far below any human-rounding step). Throws on mismatch.
207
+ */
208
+ export function assertBiasV2Semantics(data: unknown): BiasReportV2 {
209
+ const report = data as BiasReportV2;
210
+ assertPartialInvariants(report);
211
+ assertNumberMatches("result.score", recomputedBiasScore(report.pairResults), report.result.score);
212
+ assertStatisticsMatch(buildBiasStatistics(report.pairResults), report.statistics);
213
+ return report;
214
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Browser-safe subset of `@euconform/core/evidence`.
3
+ *
4
+ * Everything exported here is pure TypeScript with no Node builtins (`node:fs`,
5
+ * `node:path`, `node:url`, `node:crypto`), so it can be bundled into the web app.
6
+ * The Node-only entrypoint (`@euconform/core/evidence`) adds schema loading from
7
+ * disk, artifact writers and hashing on top of the same pure core — in particular
8
+ * the semantic check comes from the same `bias-v2-semantics.ts` module, so there
9
+ * is never a second implementation of "do these numbers follow from the data".
10
+ */
11
+
12
+ export type {
13
+ BiasComparison,
14
+ BiasComparisonPaired,
15
+ BiasPairChange,
16
+ BiasRunSummary,
17
+ BiasTypeComparison,
18
+ CompareBiasRunsOptions,
19
+ } from "./bias-comparison";
20
+ export type {
21
+ BiasStatisticsV2,
22
+ CategoryPreferenceStatistics,
23
+ Quartiles,
24
+ WilsonInterval,
25
+ } from "./bias-statistics";
26
+ export type {
27
+ BiasReportV2,
28
+ BiasReportV2Provenance,
29
+ BiasReportV2Sampling,
30
+ } from "./types";
31
+
32
+ export { compareBiasRuns, mcnemarExactPValue, summarizeBiasRun } from "./bias-comparison";
33
+ export { formatBiasComparisonMarkdown } from "./bias-comparison-markdown";
34
+ export { buildBiasStatistics, quartilesType7, wilson95, WILSON_Z95 } from "./bias-statistics";
35
+ export { assertBiasV2Semantics, recomputedBiasScore } from "./bias-v2-semantics";
@@ -10,6 +10,7 @@ interface BuildBundleOptions {
10
10
  report: ArtifactInput;
11
11
  aibom?: ArtifactInput;
12
12
  ci?: ArtifactInput;
13
+ bias?: ArtifactInput;
13
14
  summary?: ArtifactInput;
14
15
  tool: { name: string; version: string };
15
16
  target: { name: string; rootPath: string };
@@ -53,6 +54,7 @@ export function buildBundleManifest(opts: BuildBundleOptions): ScanBundle {
53
54
 
54
55
  if (opts.aibom) artifacts.push(makeArtifactRef("aibom", opts.aibom, false));
55
56
  if (opts.ci) artifacts.push(makeArtifactRef("ci", opts.ci, false));
57
+ if (opts.bias) artifacts.push(makeArtifactRef("bias", opts.bias, false));
56
58
  if (opts.summary) artifacts.push(makeArtifactRef("summary", opts.summary, false));
57
59
 
58
60
  return {
@@ -9,6 +9,10 @@ export type {
9
9
  AiBillOfMaterials,
10
10
  AppType,
11
11
  AssessmentHint,
12
+ BiasReport,
13
+ BiasReportV2,
14
+ BiasReportV2Provenance,
15
+ BiasReportV2Sampling,
12
16
  BomComponent,
13
17
  BomComponentKind,
14
18
  BundleArtifactRef,
@@ -18,6 +22,7 @@ export type {
18
22
  ConfidenceLevel,
19
23
  DetectedSignal,
20
24
  DetectorContext,
25
+ ExternalObservation,
21
26
  FailOnLevel,
22
27
  FileProvenance,
23
28
  GapCounts,
@@ -36,16 +41,42 @@ export type {
36
41
  SignalDetector,
37
42
  SignalEvidence,
38
43
  } from "./types";
44
+ export type {
45
+ BiasStatisticsV2,
46
+ CategoryPreferenceStatistics,
47
+ Quartiles,
48
+ WilsonInterval,
49
+ } from "./bias-statistics";
50
+ export type {
51
+ BiasComparison,
52
+ BiasComparisonPaired,
53
+ BiasPairChange,
54
+ BiasRunSummary,
55
+ BiasTypeComparison,
56
+ CompareBiasRunsOptions,
57
+ } from "./bias-comparison";
58
+ export type { BiasValidationResult } from "./validate";
39
59
 
40
60
  export { generateScanOutput } from "./output";
41
61
  export { generateSummaryMarkdown } from "./markdown";
42
62
  export { buildBundleManifest } from "./bundle";
43
63
  export { sha256Hex } from "./hash";
64
+ export { buildBiasStatistics, quartilesType7, wilson95, WILSON_Z95 } from "./bias-statistics";
65
+ export {
66
+ compareBiasRuns,
67
+ mcnemarExactPValue,
68
+ summarizeBiasRun,
69
+ } from "./bias-comparison";
70
+ export { formatBiasComparisonMarkdown } from "./bias-comparison-markdown";
71
+ export { validateBiasReportV2, isPartialRun } from "./validate-bias-v2";
44
72
  export {
45
73
  getSchemaMajorVersion,
46
74
  validateAiBillOfMaterials,
75
+ validateBiasArtifact,
76
+ validateBiasReport,
47
77
  validateCiReport,
48
78
  validateEcefJsonDocument,
79
+ validateExternalObservation,
49
80
  validateScanBundle,
50
81
  validateScanReport,
51
82
  } from "./validate";
@@ -81,6 +81,7 @@ const SOURCE_PRIORITY: Record<BomComponent["source"], number> = {
81
81
  config: 4,
82
82
  code: 3,
83
83
  "lock-file": 2,
84
+ "sbom-import": 2,
84
85
  docs: 1,
85
86
  };
86
87