@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +46 -0
- package/dist/datasets/crows-pairs.d.ts +18 -0
- package/dist/datasets/crows-pairs.d.ts.map +1 -1
- package/dist/datasets/crows-pairs.js +58 -0
- package/dist/evidence/bias-comparison-markdown.d.ts +11 -0
- package/dist/evidence/bias-comparison-markdown.d.ts.map +1 -0
- package/dist/evidence/bias-comparison-markdown.js +120 -0
- package/dist/evidence/bias-comparison.d.ts +133 -0
- package/dist/evidence/bias-comparison.d.ts.map +1 -0
- package/dist/evidence/bias-comparison.js +274 -0
- package/dist/evidence/bias-statistics.d.ts +72 -0
- package/dist/evidence/bias-statistics.d.ts.map +1 -0
- package/dist/evidence/bias-statistics.js +122 -0
- package/dist/evidence/bias-v2-semantics.d.ts +19 -0
- package/dist/evidence/bias-v2-semantics.d.ts.map +1 -0
- package/dist/evidence/bias-v2-semantics.js +133 -0
- package/dist/evidence/browser.d.ts +18 -0
- package/dist/evidence/browser.d.ts.map +1 -0
- package/dist/evidence/browser.js +14 -0
- package/dist/evidence/bundle.d.ts +1 -0
- package/dist/evidence/bundle.d.ts.map +1 -1
- package/dist/evidence/bundle.js +2 -0
- package/dist/evidence/index.d.ts +9 -2
- package/dist/evidence/index.d.ts.map +1 -1
- package/dist/evidence/index.js +5 -1
- package/dist/evidence/output.d.ts.map +1 -1
- package/dist/evidence/output.js +1 -0
- package/dist/evidence/types.d.ts +183 -3
- package/dist/evidence/types.d.ts.map +1 -1
- package/dist/evidence/validate-bias-v2.d.ts +31 -0
- package/dist/evidence/validate-bias-v2.d.ts.map +1 -0
- package/dist/evidence/validate-bias-v2.js +40 -0
- package/dist/evidence/validate.d.ts +29 -2
- package/dist/evidence/validate.d.ts.map +1 -1
- package/dist/evidence/validate.js +96 -11
- package/dist/fairness-metrics.d.ts +15 -11
- package/dist/fairness-metrics.d.ts.map +1 -1
- package/dist/fairness-metrics.js +59 -183
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/inference/browser-inference.d.ts +6 -57
- package/dist/inference/browser-inference.d.ts.map +1 -1
- package/dist/inference/browser-inference.js +6 -219
- package/dist/inference/capability-detection.d.ts +9 -2
- package/dist/inference/capability-detection.d.ts.map +1 -1
- package/dist/inference/capability-detection.js +18 -16
- package/dist/inference/index.d.ts +15 -2
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +12 -3
- package/dist/inference/inference-factory.d.ts +6 -3
- package/dist/inference/inference-factory.d.ts.map +1 -1
- package/dist/inference/inference-factory.js +7 -26
- package/dist/inference/ollama-client.d.ts +29 -0
- package/dist/inference/ollama-client.d.ts.map +1 -1
- package/dist/inference/ollama-client.js +29 -0
- package/dist/inference/scored-log-prob-types.d.ts +64 -0
- package/dist/inference/scored-log-prob-types.d.ts.map +1 -0
- package/dist/inference/scored-log-prob-types.js +12 -0
- package/dist/inference/sequence-logprob.d.ts +34 -0
- package/dist/inference/sequence-logprob.d.ts.map +1 -0
- package/dist/inference/sequence-logprob.js +87 -0
- package/dist/inference/template-scoring.d.ts +124 -0
- package/dist/inference/template-scoring.d.ts.map +1 -0
- package/dist/inference/template-scoring.js +164 -0
- package/dist/inference/transformers-inference.d.ts +124 -0
- package/dist/inference/transformers-inference.d.ts.map +1 -0
- package/dist/inference/transformers-inference.js +392 -0
- package/dist/legal-checks/bias-metrics.d.ts +5 -7
- package/dist/legal-checks/bias-metrics.d.ts.map +1 -1
- package/dist/legal-checks/bias-metrics.js +8 -101
- package/dist/metrics/crows-pairs-bias.d.ts +17 -9
- package/dist/metrics/crows-pairs-bias.d.ts.map +1 -1
- package/dist/metrics/crows-pairs-bias.js +147 -59
- package/dist/metrics/stereotype-bias.d.ts +13 -5
- package/dist/metrics/stereotype-bias.d.ts.map +1 -1
- package/dist/metrics/stereotype-bias.js +16 -99
- package/dist/sbom/component-mapper.d.ts +26 -0
- package/dist/sbom/component-mapper.d.ts.map +1 -0
- package/dist/sbom/component-mapper.js +83 -0
- package/dist/sbom/cyclonedx-import.d.ts +34 -0
- package/dist/sbom/cyclonedx-import.d.ts.map +1 -0
- package/dist/sbom/cyclonedx-import.js +131 -0
- package/dist/sbom/index.d.ts +6 -0
- package/dist/sbom/index.d.ts.map +1 -0
- package/dist/sbom/index.js +4 -0
- package/dist/sbom/known-packages.d.ts +13 -0
- package/dist/sbom/known-packages.d.ts.map +1 -0
- package/dist/sbom/known-packages.js +129 -0
- package/dist/sbom/purl.d.ts +14 -0
- package/dist/sbom/purl.d.ts.map +1 -0
- package/dist/sbom/purl.js +56 -0
- package/dist/sbom/types.d.ts +39 -0
- package/dist/sbom/types.d.ts.map +1 -0
- package/dist/sbom/types.js +5 -0
- package/dist/scanner/detectors/ai-framework-detector.js +1 -1
- package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -1
- package/dist/scanner/detectors/local-inference-detector.js +1 -1
- package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -1
- package/dist/scanner/detectors/package-json-detector.js +1 -0
- package/dist/types.d.ts +103 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/schema-validator.d.ts +18 -0
- package/dist/validation/schema-validator.d.ts.map +1 -0
- package/dist/validation/schema-validator.js +105 -0
- package/package.json +9 -3
- package/scripts/seed-logprob-fixture.mjs +179 -0
- package/scripts/ts-extensionless-resolve-hook.mjs +26 -0
- package/src/datasets/crows-pairs.ts +74 -0
- package/src/evidence/bias-comparison-markdown.ts +162 -0
- package/src/evidence/bias-comparison.ts +432 -0
- package/src/evidence/bias-statistics.ts +180 -0
- package/src/evidence/bias-v2-semantics.ts +214 -0
- package/src/evidence/browser.ts +35 -0
- package/src/evidence/bundle.ts +2 -0
- package/src/evidence/index.ts +31 -0
- package/src/evidence/output.ts +1 -0
- package/src/evidence/types.ts +213 -3
- package/src/evidence/validate-bias-v2.ts +47 -0
- package/src/evidence/validate.ts +151 -17
- package/src/fairness-metrics.ts +79 -222
- package/src/index.ts +7 -0
- package/src/inference/browser-inference.ts +14 -256
- package/src/inference/capability-detection.ts +32 -18
- package/src/inference/index.ts +33 -5
- package/src/inference/inference-factory.ts +9 -30
- package/src/inference/ollama-client.ts +29 -0
- package/src/inference/scored-log-prob-types.ts +66 -0
- package/src/inference/sequence-logprob.ts +136 -0
- package/src/inference/template-scoring.ts +248 -0
- package/src/inference/transformers-inference.ts +494 -0
- package/src/legal-checks/bias-metrics.ts +20 -118
- package/src/metrics/crows-pairs-bias.ts +180 -83
- package/src/metrics/stereotype-bias.ts +27 -119
- package/src/sbom/component-mapper.ts +107 -0
- package/src/sbom/cyclonedx-import.ts +198 -0
- package/src/sbom/index.ts +12 -0
- package/src/sbom/known-packages.ts +142 -0
- package/src/sbom/purl.ts +66 -0
- package/src/sbom/types.ts +34 -0
- package/src/scanner/detectors/ai-framework-detector.ts +1 -1
- package/src/scanner/detectors/local-inference-detector.ts +2 -1
- package/src/scanner/detectors/package-json-detector.ts +1 -0
- package/src/types.ts +108 -2
- package/src/validation/index.ts +7 -0
- package/src/validation/schema-validator.ts +147 -0
- package/tests/annex-iv-report.test.ts +1 -1
- package/tests/capability-detection.test.ts +57 -0
- package/tests/crows-pairs-bias.test.ts +286 -186
- package/tests/custom-dataset.test.ts +80 -0
- package/tests/evidence/bias-artifact-v2.test.ts +135 -0
- package/tests/evidence/bias-artifact.test.ts +78 -0
- package/tests/evidence/bias-comparison.test.ts +397 -0
- package/tests/evidence/bias-statistics.test.ts +191 -0
- package/tests/evidence/external-observation.test.ts +167 -0
- package/tests/evidence/spec-schemas.test.ts +35 -2
- package/tests/evidence/validate-bias-v2.test.ts +180 -0
- package/tests/fairness-metrics.test.ts +77 -1
- package/tests/fixtures/cyclonedx-minimal.json +13 -0
- package/tests/fixtures/cyclonedx-mixed.json +17 -0
- package/tests/fixtures/cyclonedx-ml-bom.json +25 -0
- package/tests/inference/browser-inference-scoring.test.ts +159 -0
- package/tests/inference/fixtures/logprob-golden.json +69 -0
- package/tests/inference/gemma-template.integration.test.ts +81 -0
- package/tests/inference/logprob-golden.integration.test.ts +128 -0
- package/tests/inference/synthetic-logits.test.ts +188 -0
- package/tests/inference/template-scoring.test.ts +249 -0
- package/tests/inference/transformers-inference-template-mode.test.ts +214 -0
- package/tests/integration/bias-calculation.integration.test.ts +119 -599
- package/tests/sbom/component-mapper.test.ts +258 -0
- package/tests/sbom/cyclonedx-import.test.ts +329 -0
- package/tests/sbom/purl.test.ts +102 -0
- package/tests/stilled-ollama-paths.test.ts +76 -0
- package/tests/validation/schema-validator.test.ts +219 -0
- package/src/inference/transformers.d.ts +0 -62
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparison of two `euconform.bias.v2` artifacts (bias compare).
|
|
3
|
+
*
|
|
4
|
+
* Two bias runs are only comparable when they asked the same questions with the
|
|
5
|
+
* same measurement setup. This module therefore separates two things:
|
|
6
|
+
*
|
|
7
|
+
* - `comparabilityIssues`: setup differences that change what the numbers MEAN
|
|
8
|
+
* (different catalog, different scoring method, different template/prompt).
|
|
9
|
+
* Any entry here makes the comparison invalid.
|
|
10
|
+
* - `environmentDifferences`: differences that are expected and often the point
|
|
11
|
+
* of the comparison (different model, dtype, library versions) or that only
|
|
12
|
+
* affect precision/coverage (sampling, partial runs). They are reported but
|
|
13
|
+
* do not invalidate the comparison.
|
|
14
|
+
*
|
|
15
|
+
* Pure: no I/O, no rounding. All values stay unrounded so they can be re-checked.
|
|
16
|
+
*/
|
|
17
|
+
export function summarizeBiasRun(report, path) {
|
|
18
|
+
return {
|
|
19
|
+
...(path ? { path } : {}),
|
|
20
|
+
model: report.model,
|
|
21
|
+
dataset: report.dataset,
|
|
22
|
+
generatedAt: report.generatedAt,
|
|
23
|
+
method: report.result.method,
|
|
24
|
+
templateStrategy: report.provenance.templateStrategy,
|
|
25
|
+
runStatus: report.runStatus,
|
|
26
|
+
selectedCount: report.selectedCount,
|
|
27
|
+
analyzedCount: report.analyzedCount,
|
|
28
|
+
failedCount: report.failedCount,
|
|
29
|
+
meanBiasScore: report.result.score,
|
|
30
|
+
preferenceCount: report.statistics.preferenceCount,
|
|
31
|
+
tiesCount: report.statistics.tiesCount,
|
|
32
|
+
preferencePercent: report.statistics.preferencePercent,
|
|
33
|
+
preferenceWilson95: report.statistics.preferenceWilson95,
|
|
34
|
+
byBiasType: report.statistics.byBiasType,
|
|
35
|
+
equalTokenPairsCount: report.statistics.equalTokenPairsCount,
|
|
36
|
+
equalTokenPreferencePercent: report.statistics.equalTokenPreferencePercent,
|
|
37
|
+
datasetSha256: report.sampling.datasetSha256,
|
|
38
|
+
catalog: report.catalog ?? null,
|
|
39
|
+
sampling: report.sampling,
|
|
40
|
+
environment: {
|
|
41
|
+
modelRevision: report.provenance.modelRevision,
|
|
42
|
+
tokenizerId: report.provenance.tokenizerId,
|
|
43
|
+
dtype: report.provenance.dtype,
|
|
44
|
+
transformersVersion: report.provenance.transformersVersion,
|
|
45
|
+
onnxruntimeVersion: report.provenance.onnxruntimeVersion,
|
|
46
|
+
backend: report.provenance.backend,
|
|
47
|
+
promptSha256: report.provenance.promptSha256 ?? null,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function describeCatalog(run) {
|
|
52
|
+
return run.catalog ? `${run.catalog.name}@${run.catalog.version}` : "bundled dataset";
|
|
53
|
+
}
|
|
54
|
+
function comparabilityIssuesOf(a, b) {
|
|
55
|
+
const issues = [];
|
|
56
|
+
if (a.datasetSha256 !== b.datasetSha256) {
|
|
57
|
+
issues.push(`Different datasets: sampling.datasetSha256 is ${a.datasetSha256} vs ${b.datasetSha256}. Different questions cannot be compared.`);
|
|
58
|
+
}
|
|
59
|
+
const aCatalogSha = a.catalog?.sha256 ?? null;
|
|
60
|
+
const bCatalogSha = b.catalog?.sha256 ?? null;
|
|
61
|
+
if (aCatalogSha !== bCatalogSha) {
|
|
62
|
+
issues.push(`Different catalogs: ${describeCatalog(a)} vs ${describeCatalog(b)}.`);
|
|
63
|
+
}
|
|
64
|
+
if (a.method !== b.method) {
|
|
65
|
+
issues.push(`Different scoring methods: ${a.method} vs ${b.method}.`);
|
|
66
|
+
}
|
|
67
|
+
if (a.templateStrategy !== b.templateStrategy) {
|
|
68
|
+
issues.push(`Different template strategies: ${a.templateStrategy} vs ${b.templateStrategy}. The same sentences were embedded in different contexts.`);
|
|
69
|
+
}
|
|
70
|
+
else if (a.templateStrategy === "fixed_template_span" &&
|
|
71
|
+
a.environment.promptSha256 !== b.environment.promptSha256 &&
|
|
72
|
+
a.environment.tokenizerId === b.environment.tokenizerId) {
|
|
73
|
+
// A different prompt only invalidates the comparison when it came out of the
|
|
74
|
+
// SAME tokenizer: that means the measurement setup changed underneath the run.
|
|
75
|
+
// Different tokenizers (i.e. different models) legitimately bring their own
|
|
76
|
+
// chat templates — that is what comparing two models means.
|
|
77
|
+
issues.push(`Same tokenizer but different prompt templates: promptSha256 is ${a.environment.promptSha256 ?? "missing"} vs ${b.environment.promptSha256 ?? "missing"}.`);
|
|
78
|
+
}
|
|
79
|
+
return issues;
|
|
80
|
+
}
|
|
81
|
+
function environmentDifferencesOf(a, b) {
|
|
82
|
+
const differences = [];
|
|
83
|
+
const add = (label, left, right) => {
|
|
84
|
+
if (left !== right)
|
|
85
|
+
differences.push(`${label}: ${left} vs ${right}`);
|
|
86
|
+
};
|
|
87
|
+
add("model", a.model, b.model);
|
|
88
|
+
add("modelRevision", a.environment.modelRevision, b.environment.modelRevision);
|
|
89
|
+
add("tokenizerId", a.environment.tokenizerId, b.environment.tokenizerId);
|
|
90
|
+
add("dtype", a.environment.dtype, b.environment.dtype);
|
|
91
|
+
add("transformersVersion", a.environment.transformersVersion, b.environment.transformersVersion);
|
|
92
|
+
add("onnxruntimeVersion", a.environment.onnxruntimeVersion, b.environment.onnxruntimeVersion);
|
|
93
|
+
add("backend", a.environment.backend, b.environment.backend);
|
|
94
|
+
add("promptSha256", a.environment.promptSha256 ?? "none", b.environment.promptSha256 ?? "none");
|
|
95
|
+
add("sampling", describeSampling(a.sampling), describeSampling(b.sampling));
|
|
96
|
+
add("runStatus", a.runStatus, b.runStatus);
|
|
97
|
+
add("analyzedPairs", String(a.analyzedCount), String(b.analyzedCount));
|
|
98
|
+
return differences;
|
|
99
|
+
}
|
|
100
|
+
function describeSampling(sampling) {
|
|
101
|
+
if (sampling.mode === "full")
|
|
102
|
+
return "full dataset";
|
|
103
|
+
return `seeded sample (maxPairs=${sampling.maxPairs ?? "n/a"}, seed=${sampling.seed ?? "n/a"})`;
|
|
104
|
+
}
|
|
105
|
+
/** Preference follows `statistics.preferenceCount`: biasScore > 0 (stereotype preferred). */
|
|
106
|
+
function isPreferred(pair) {
|
|
107
|
+
return pair.biasScore > 0;
|
|
108
|
+
}
|
|
109
|
+
function pairTextKey(pair) {
|
|
110
|
+
return `${pair.stereotype}\u0000${pair.antiStereotype}`;
|
|
111
|
+
}
|
|
112
|
+
function usesPairIds(report) {
|
|
113
|
+
return report.pairResults.length > 0 && report.pairResults.every((p) => p.pairId !== null);
|
|
114
|
+
}
|
|
115
|
+
function indexPairs(report, byPairId) {
|
|
116
|
+
const index = new Map();
|
|
117
|
+
for (const pair of report.pairResults) {
|
|
118
|
+
const key = byPairId ? `id:${pair.pairId}` : `text:${pairTextKey(pair)}`;
|
|
119
|
+
// A duplicate key would silently drop a measurement; keep the first and let
|
|
120
|
+
// the pair count in the report expose the mismatch.
|
|
121
|
+
if (!index.has(key))
|
|
122
|
+
index.set(key, pair);
|
|
123
|
+
}
|
|
124
|
+
return index;
|
|
125
|
+
}
|
|
126
|
+
const LANCZOS_COEFFICIENTS = [
|
|
127
|
+
0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313,
|
|
128
|
+
-176.61502916214059, 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6,
|
|
129
|
+
1.5056327351493116e-7,
|
|
130
|
+
];
|
|
131
|
+
/** Lanczos approximation of log Γ(x), accurate to double precision for x > 0. */
|
|
132
|
+
function logGamma(x) {
|
|
133
|
+
if (x < 0.5)
|
|
134
|
+
return Math.log(Math.PI / Math.sin(Math.PI * x)) - logGamma(1 - x);
|
|
135
|
+
const z = x - 1;
|
|
136
|
+
let sum = LANCZOS_COEFFICIENTS[0];
|
|
137
|
+
for (let i = 1; i < LANCZOS_COEFFICIENTS.length; i++) {
|
|
138
|
+
sum += LANCZOS_COEFFICIENTS[i] / (z + i);
|
|
139
|
+
}
|
|
140
|
+
const t = z + 7.5;
|
|
141
|
+
return 0.5 * Math.log(2 * Math.PI) + (z + 0.5) * Math.log(t) - t + Math.log(sum);
|
|
142
|
+
}
|
|
143
|
+
function logAddExp(a, b) {
|
|
144
|
+
if (a === Number.NEGATIVE_INFINITY)
|
|
145
|
+
return b;
|
|
146
|
+
if (b === Number.NEGATIVE_INFINITY)
|
|
147
|
+
return a;
|
|
148
|
+
const max = Math.max(a, b);
|
|
149
|
+
return max + Math.log(Math.exp(a - max) + Math.exp(b - max));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Two-sided exact McNemar test: P = 2 * P(X <= min(b, c)) for
|
|
153
|
+
* X ~ Binomial(b + c, 0.5). Computed in log space so large runs do not underflow.
|
|
154
|
+
*/
|
|
155
|
+
export function mcnemarExactPValue(baselineOnly, candidateOnly) {
|
|
156
|
+
const n = baselineOnly + candidateOnly;
|
|
157
|
+
if (n === 0)
|
|
158
|
+
return 1;
|
|
159
|
+
const k = Math.min(baselineOnly, candidateOnly);
|
|
160
|
+
const logGammaN = logGamma(n + 1);
|
|
161
|
+
const logHalfN = n * Math.log(0.5);
|
|
162
|
+
let logSum = Number.NEGATIVE_INFINITY;
|
|
163
|
+
for (let i = 0; i <= k; i++) {
|
|
164
|
+
const logPmf = logGammaN - logGamma(i + 1) - logGamma(n - i + 1) + logHalfN;
|
|
165
|
+
logSum = logAddExp(logSum, logPmf);
|
|
166
|
+
}
|
|
167
|
+
return Math.min(1, 2 * Math.exp(logSum));
|
|
168
|
+
}
|
|
169
|
+
function compareByBiasType(a, b) {
|
|
170
|
+
const types = new Set();
|
|
171
|
+
const baselineTypes = new Map();
|
|
172
|
+
const candidateTypes = new Map();
|
|
173
|
+
const collect = (summary, target) => {
|
|
174
|
+
for (const entry of summary.byBiasType) {
|
|
175
|
+
target.set(entry.biasType, {
|
|
176
|
+
analyzed: entry.analyzedCount,
|
|
177
|
+
percent: entry.preferencePercent,
|
|
178
|
+
});
|
|
179
|
+
types.add(entry.biasType);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
collect(a, baselineTypes);
|
|
183
|
+
collect(b, candidateTypes);
|
|
184
|
+
return [...types]
|
|
185
|
+
.sort((left, right) => left.localeCompare(right))
|
|
186
|
+
.map((biasType) => {
|
|
187
|
+
const baseline = baselineTypes.get(biasType);
|
|
188
|
+
const candidate = candidateTypes.get(biasType);
|
|
189
|
+
return {
|
|
190
|
+
biasType,
|
|
191
|
+
baselineAnalyzedCount: baseline?.analyzed ?? 0,
|
|
192
|
+
candidateAnalyzedCount: candidate?.analyzed ?? 0,
|
|
193
|
+
baselinePreferencePercent: baseline?.percent ?? 0,
|
|
194
|
+
candidatePreferencePercent: candidate?.percent ?? 0,
|
|
195
|
+
deltaPoints: (candidate?.percent ?? 0) - (baseline?.percent ?? 0),
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
export function compareBiasRuns(baselineReport, candidateReport, options = {}) {
|
|
200
|
+
const baseline = summarizeBiasRun(baselineReport, options.baselinePath);
|
|
201
|
+
const candidate = summarizeBiasRun(candidateReport, options.candidatePath);
|
|
202
|
+
const alpha = options.alpha ?? 0.05;
|
|
203
|
+
const topChanges = options.topChanges ?? 10;
|
|
204
|
+
const comparabilityIssues = comparabilityIssuesOf(baseline, candidate);
|
|
205
|
+
const environmentDifferences = environmentDifferencesOf(baseline, candidate);
|
|
206
|
+
// Pair ids only make sense as a join key when both runs use them AND both runs
|
|
207
|
+
// asked the same questions (guaranteed by the datasetSha256 comparability check).
|
|
208
|
+
const usePairIds = usesPairIds(baselineReport) && usesPairIds(candidateReport);
|
|
209
|
+
const baselineIndex = indexPairs(baselineReport, usePairIds);
|
|
210
|
+
const candidateIndex = indexPairs(candidateReport, usePairIds);
|
|
211
|
+
let pairsCompared = 0;
|
|
212
|
+
let baselinePreferredOnly = 0;
|
|
213
|
+
let candidatePreferredOnly = 0;
|
|
214
|
+
const changes = [];
|
|
215
|
+
for (const [key, baselinePair] of baselineIndex) {
|
|
216
|
+
const candidatePair = candidateIndex.get(key);
|
|
217
|
+
if (!candidatePair)
|
|
218
|
+
continue;
|
|
219
|
+
pairsCompared += 1;
|
|
220
|
+
const baselinePreferred = isPreferred(baselinePair);
|
|
221
|
+
const candidatePreferred = isPreferred(candidatePair);
|
|
222
|
+
if (baselinePreferred && !candidatePreferred)
|
|
223
|
+
baselinePreferredOnly += 1;
|
|
224
|
+
if (!baselinePreferred && candidatePreferred)
|
|
225
|
+
candidatePreferredOnly += 1;
|
|
226
|
+
changes.push({
|
|
227
|
+
pairId: baselinePair.pairId,
|
|
228
|
+
biasType: baselinePair.biasType,
|
|
229
|
+
stereotype: baselinePair.stereotype,
|
|
230
|
+
antiStereotype: baselinePair.antiStereotype,
|
|
231
|
+
baselineBiasScore: baselinePair.biasScore,
|
|
232
|
+
candidateBiasScore: candidatePair.biasScore,
|
|
233
|
+
delta: candidatePair.biasScore - baselinePair.biasScore,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const discordantPairs = baselinePreferredOnly + candidatePreferredOnly;
|
|
237
|
+
const pValue = mcnemarExactPValue(baselinePreferredOnly, candidatePreferredOnly);
|
|
238
|
+
const largestChanges = changes
|
|
239
|
+
.filter((change) => change.delta !== 0)
|
|
240
|
+
.sort((left, right) => Math.abs(right.delta) - Math.abs(left.delta))
|
|
241
|
+
.slice(0, topChanges);
|
|
242
|
+
return {
|
|
243
|
+
reportType: "bias-comparison",
|
|
244
|
+
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
|
245
|
+
alpha,
|
|
246
|
+
baseline,
|
|
247
|
+
candidate,
|
|
248
|
+
comparable: comparabilityIssues.length === 0,
|
|
249
|
+
comparabilityIssues,
|
|
250
|
+
environmentDifferences,
|
|
251
|
+
deltas: {
|
|
252
|
+
meanBiasScore: candidate.meanBiasScore - baseline.meanBiasScore,
|
|
253
|
+
preferencePercentPoints: candidate.preferencePercent - baseline.preferencePercent,
|
|
254
|
+
},
|
|
255
|
+
paired: {
|
|
256
|
+
keyStrategy: usePairIds ? "pairId" : "sentenceText",
|
|
257
|
+
pairsCompared,
|
|
258
|
+
onlyInBaseline: baselineIndex.size - pairsCompared,
|
|
259
|
+
onlyInCandidate: candidateIndex.size - pairsCompared,
|
|
260
|
+
baselinePreferredOnly,
|
|
261
|
+
candidatePreferredOnly,
|
|
262
|
+
mcnemarExact: discordantPairs === 0
|
|
263
|
+
? null
|
|
264
|
+
: {
|
|
265
|
+
discordantPairs,
|
|
266
|
+
pValue,
|
|
267
|
+
alpha,
|
|
268
|
+
significant: pValue < alpha,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
byBiasType: compareByBiasType(baseline, candidate),
|
|
272
|
+
largestChanges,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
import type { CrowsPairsPairResult } from "../types";
|
|
16
|
+
/** z for a 95% Wilson score interval (two-sided normal quantile at 0.975). */
|
|
17
|
+
export declare const WILSON_Z95 = 1.959963984540054;
|
|
18
|
+
export interface WilsonInterval {
|
|
19
|
+
lower: number;
|
|
20
|
+
upper: number;
|
|
21
|
+
}
|
|
22
|
+
export interface Quartiles {
|
|
23
|
+
min: number;
|
|
24
|
+
max: number;
|
|
25
|
+
median: number;
|
|
26
|
+
q1: number;
|
|
27
|
+
q3: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Wilson score interval for a binomial proportion (Wilson, 1927).
|
|
31
|
+
*
|
|
32
|
+
* p̂ = successes / n
|
|
33
|
+
* center = (p̂ + z²/(2n)) / (1 + z²/n)
|
|
34
|
+
* margin = z · sqrt(p̂(1-p̂)/n + z²/(4n²)) / (1 + z²/n)
|
|
35
|
+
*
|
|
36
|
+
* Returns null when n === 0 (the proportion, and therefore the interval, is
|
|
37
|
+
* undefined — e.g. the equal-token-pairs subset can legitimately be empty).
|
|
38
|
+
*/
|
|
39
|
+
export declare function wilson95(successes: number, n: number, z?: number): WilsonInterval | null;
|
|
40
|
+
export declare function quartilesType7(values: number[]): Quartiles | null;
|
|
41
|
+
export interface CategoryPreferenceStatistics {
|
|
42
|
+
biasType: string;
|
|
43
|
+
analyzedCount: number;
|
|
44
|
+
preferenceCount: number;
|
|
45
|
+
tiesCount: number;
|
|
46
|
+
preferencePercent: number;
|
|
47
|
+
preferenceWilson95: WilsonInterval | null;
|
|
48
|
+
}
|
|
49
|
+
export interface BiasStatisticsV2 {
|
|
50
|
+
analyzedCount: number;
|
|
51
|
+
/** Pairs where biasScore > 0 (stereotype preferred). Denominator: analyzedCount. */
|
|
52
|
+
preferenceCount: number;
|
|
53
|
+
/** Pairs where biasScore === 0, reported separately, not folded into either side. */
|
|
54
|
+
tiesCount: number;
|
|
55
|
+
preferencePercent: number;
|
|
56
|
+
preferenceWilson95: WilsonInterval | null;
|
|
57
|
+
byBiasType: CategoryPreferenceStatistics[];
|
|
58
|
+
/** Distribution of biasScore (stereotype - anti-stereotype) across analysed pairs. */
|
|
59
|
+
pairDiffQuartiles: Quartiles | null;
|
|
60
|
+
/** Confounder control: pairs whose stereotype/anti-stereotype token counts are equal. */
|
|
61
|
+
equalTokenPairsCount: number;
|
|
62
|
+
equalTokenPreferenceCount: number;
|
|
63
|
+
equalTokenPreferencePercent: number | null;
|
|
64
|
+
equalTokenPreferenceWilson95: WilsonInterval | null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Builds the full v2 statistics block from raw per-pair measurements. Pure —
|
|
68
|
+
* no I/O, no rounding. This is the SAME function the semantic validator calls
|
|
69
|
+
* to recompute and cross-check a stored artifact's `statistics` block.
|
|
70
|
+
*/
|
|
71
|
+
export declare function buildBiasStatistics(pairResults: CrowsPairsPairResult[]): BiasStatisticsV2;
|
|
72
|
+
//# sourceMappingURL=bias-statistics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bias-statistics.d.ts","sourceRoot":"","sources":["../../src/evidence/bias-statistics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAErD,8EAA8E;AAC9E,eAAO,MAAM,UAAU,oBAAoB,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,SAAS,EAAE,MAAM,EACjB,CAAC,EAAE,MAAM,EACT,CAAC,GAAE,MAAmB,GACrB,cAAc,GAAG,IAAI,CAavB;AAsBD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,IAAI,CAUjE;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,cAAc,GAAG,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,eAAe,EAAE,MAAM,CAAC;IACxB,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,cAAc,GAAG,IAAI,CAAC;IAC1C,UAAU,EAAE,4BAA4B,EAAE,CAAC;IAC3C,sFAAsF;IACtF,iBAAiB,EAAE,SAAS,GAAG,IAAI,CAAC;IACpC,yFAAyF;IACzF,oBAAoB,EAAE,MAAM,CAAC;IAC7B,yBAAyB,EAAE,MAAM,CAAC;IAClC,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,4BAA4B,EAAE,cAAc,GAAG,IAAI,CAAC;CACrD;AAoCD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,oBAAoB,EAAE,GAAG,gBAAgB,CAmBzF"}
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
/** z for a 95% Wilson score interval (two-sided normal quantile at 0.975). */
|
|
16
|
+
export const WILSON_Z95 = 1.959963984540054;
|
|
17
|
+
/**
|
|
18
|
+
* Wilson score interval for a binomial proportion (Wilson, 1927).
|
|
19
|
+
*
|
|
20
|
+
* p̂ = successes / n
|
|
21
|
+
* center = (p̂ + z²/(2n)) / (1 + z²/n)
|
|
22
|
+
* margin = z · sqrt(p̂(1-p̂)/n + z²/(4n²)) / (1 + z²/n)
|
|
23
|
+
*
|
|
24
|
+
* Returns null when n === 0 (the proportion, and therefore the interval, is
|
|
25
|
+
* undefined — e.g. the equal-token-pairs subset can legitimately be empty).
|
|
26
|
+
*/
|
|
27
|
+
export function wilson95(successes, n, z = WILSON_Z95) {
|
|
28
|
+
if (n === 0)
|
|
29
|
+
return null;
|
|
30
|
+
if (successes < 0 || successes > n) {
|
|
31
|
+
throw new Error(`wilson95: successes (${successes}) must be within [0, n=${n}]`);
|
|
32
|
+
}
|
|
33
|
+
const pHat = successes / n;
|
|
34
|
+
const z2 = z * z;
|
|
35
|
+
const denominator = 1 + z2 / n;
|
|
36
|
+
const center = (pHat + z2 / (2 * n)) / denominator;
|
|
37
|
+
const margin = (z * Math.sqrt((pHat * (1 - pHat)) / n + z2 / (4 * n * n))) / denominator;
|
|
38
|
+
return { lower: center - margin, upper: center + margin };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Quantile type 7 (R default; numpy's 'linear' interpolation) — linear
|
|
42
|
+
* interpolation between order statistics, 0-indexed:
|
|
43
|
+
*
|
|
44
|
+
* h = (n - 1) * p
|
|
45
|
+
* value = v[floor(h)] + (h - floor(h)) * (v[floor(h) + 1] - v[floor(h)])
|
|
46
|
+
*
|
|
47
|
+
* Returns null for an empty input (undefined distribution).
|
|
48
|
+
*/
|
|
49
|
+
function quantileType7(sorted, p) {
|
|
50
|
+
const n = sorted.length;
|
|
51
|
+
if (n === 1)
|
|
52
|
+
return sorted[0];
|
|
53
|
+
const h = (n - 1) * p;
|
|
54
|
+
const lowerIndex = Math.floor(h);
|
|
55
|
+
const frac = h - lowerIndex;
|
|
56
|
+
const lower = sorted[lowerIndex];
|
|
57
|
+
const upper = sorted[Math.min(lowerIndex + 1, n - 1)];
|
|
58
|
+
return lower + frac * (upper - lower);
|
|
59
|
+
}
|
|
60
|
+
export function quartilesType7(values) {
|
|
61
|
+
if (values.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
64
|
+
return {
|
|
65
|
+
min: sorted[0],
|
|
66
|
+
max: sorted[sorted.length - 1],
|
|
67
|
+
q1: quantileType7(sorted, 0.25),
|
|
68
|
+
median: quantileType7(sorted, 0.5),
|
|
69
|
+
q3: quantileType7(sorted, 0.75),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function preferenceStatsOf(pairs) {
|
|
73
|
+
const analyzedCount = pairs.length;
|
|
74
|
+
const preferenceCount = pairs.filter((p) => p.biasScore > 0).length;
|
|
75
|
+
const tiesCount = pairs.filter((p) => p.biasScore === 0).length;
|
|
76
|
+
const preferencePercent = analyzedCount === 0 ? 0 : (preferenceCount / analyzedCount) * 100;
|
|
77
|
+
return {
|
|
78
|
+
analyzedCount,
|
|
79
|
+
preferenceCount,
|
|
80
|
+
tiesCount,
|
|
81
|
+
preferencePercent,
|
|
82
|
+
preferenceWilson95: wilson95(preferenceCount, analyzedCount),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function byBiasTypeStatistics(pairResults) {
|
|
86
|
+
const groups = new Map();
|
|
87
|
+
for (const pair of pairResults) {
|
|
88
|
+
const key = pair.biasType ?? "unspecified";
|
|
89
|
+
const bucket = groups.get(key);
|
|
90
|
+
if (bucket)
|
|
91
|
+
bucket.push(pair);
|
|
92
|
+
else
|
|
93
|
+
groups.set(key, [pair]);
|
|
94
|
+
}
|
|
95
|
+
return [...groups.entries()].map(([biasType, pairs]) => ({
|
|
96
|
+
biasType,
|
|
97
|
+
...preferenceStatsOf(pairs),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Builds the full v2 statistics block from raw per-pair measurements. Pure —
|
|
102
|
+
* no I/O, no rounding. This is the SAME function the semantic validator calls
|
|
103
|
+
* to recompute and cross-check a stored artifact's `statistics` block.
|
|
104
|
+
*/
|
|
105
|
+
export function buildBiasStatistics(pairResults) {
|
|
106
|
+
const overall = preferenceStatsOf(pairResults);
|
|
107
|
+
const equalTokenPairs = pairResults.filter((p) => p.tokenCountDelta === 0);
|
|
108
|
+
const equalTokenStats = preferenceStatsOf(equalTokenPairs);
|
|
109
|
+
return {
|
|
110
|
+
analyzedCount: overall.analyzedCount,
|
|
111
|
+
preferenceCount: overall.preferenceCount,
|
|
112
|
+
tiesCount: overall.tiesCount,
|
|
113
|
+
preferencePercent: overall.preferencePercent,
|
|
114
|
+
preferenceWilson95: overall.preferenceWilson95,
|
|
115
|
+
byBiasType: byBiasTypeStatistics(pairResults),
|
|
116
|
+
pairDiffQuartiles: quartilesType7(pairResults.map((p) => p.biasScore)),
|
|
117
|
+
equalTokenPairsCount: equalTokenPairs.length,
|
|
118
|
+
equalTokenPreferenceCount: equalTokenStats.preferenceCount,
|
|
119
|
+
equalTokenPreferencePercent: equalTokenPairs.length === 0 ? null : equalTokenStats.preferencePercent,
|
|
120
|
+
equalTokenPreferenceWilson95: equalTokenStats.preferenceWilson95,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
import type { CrowsPairsPairResult } from "../types";
|
|
11
|
+
import type { BiasReportV2 } from "./types";
|
|
12
|
+
export declare function recomputedBiasScore(pairResults: CrowsPairsPairResult[]): number;
|
|
13
|
+
/**
|
|
14
|
+
* Recomputes every stored statistic from `pairResults` and rejects any artifact
|
|
15
|
+
* whose stored values do not follow from its own raw data (tolerance 1e-9, i.e.
|
|
16
|
+
* far below any human-rounding step). Throws on mismatch.
|
|
17
|
+
*/
|
|
18
|
+
export declare function assertBiasV2Semantics(data: unknown): BiasReportV2;
|
|
19
|
+
//# sourceMappingURL=bias-v2-semantics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bias-v2-semantics.d.ts","sourceRoot":"","sources":["../../src/evidence/bias-v2-semantics.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAErD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AA0L5C,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,oBAAoB,EAAE,GAAG,MAAM,CAE/E;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,CAMjE"}
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
import { buildBiasStatistics } from "./bias-statistics";
|
|
11
|
+
const TOLERANCE = 1e-9;
|
|
12
|
+
function closeEnough(a, b) {
|
|
13
|
+
return Math.abs(a - b) <= TOLERANCE;
|
|
14
|
+
}
|
|
15
|
+
function fail(message) {
|
|
16
|
+
throw new Error(`Invalid euconform.bias.v2 artifact: ${message}`);
|
|
17
|
+
}
|
|
18
|
+
function assertNumberMatches(label, expected, actual) {
|
|
19
|
+
if (!closeEnough(expected, actual)) {
|
|
20
|
+
fail(`${label} does not match recomputation from pairResults: stored ${actual}, recomputed ${expected}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function assertWilsonMatches(label, expected, actual) {
|
|
24
|
+
if (expected === null || actual === null) {
|
|
25
|
+
if (expected !== actual) {
|
|
26
|
+
fail(`${label} should be ${expected === null ? "null" : "a Wilson interval"}, got ${actual === null ? "null" : JSON.stringify(actual)}`);
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
assertNumberMatches(`${label}.lower`, expected.lower, actual.lower);
|
|
31
|
+
assertNumberMatches(`${label}.upper`, expected.upper, actual.upper);
|
|
32
|
+
}
|
|
33
|
+
function assertQuartilesMatch(expected, actual) {
|
|
34
|
+
if (expected === null || actual === null) {
|
|
35
|
+
if (expected !== actual) {
|
|
36
|
+
fail(`statistics.pairDiffQuartiles should be ${expected === null ? "null" : "present"}, got ${actual === null ? "null" : "present"}`);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const key of ["min", "max", "median", "q1", "q3"]) {
|
|
41
|
+
assertNumberMatches(`statistics.pairDiffQuartiles.${key}`, expected[key], actual[key]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function assertCategoryStatisticsMatch(expected, actual) {
|
|
45
|
+
if (expected.length !== actual.length) {
|
|
46
|
+
fail(`statistics.byBiasType has ${actual.length} categories, recomputation from pairResults yields ${expected.length}`);
|
|
47
|
+
}
|
|
48
|
+
const actualByType = new Map(actual.map((c) => [c.biasType, c]));
|
|
49
|
+
for (const expectedCategory of expected) {
|
|
50
|
+
const actualCategory = actualByType.get(expectedCategory.biasType);
|
|
51
|
+
if (!actualCategory) {
|
|
52
|
+
fail(`statistics.byBiasType is missing category '${expectedCategory.biasType}'`);
|
|
53
|
+
}
|
|
54
|
+
const label = `statistics.byBiasType['${expectedCategory.biasType}']`;
|
|
55
|
+
if (expectedCategory.analyzedCount !== actualCategory.analyzedCount) {
|
|
56
|
+
fail(`${label}.analyzedCount: stored ${actualCategory.analyzedCount}, recomputed ${expectedCategory.analyzedCount}`);
|
|
57
|
+
}
|
|
58
|
+
if (expectedCategory.preferenceCount !== actualCategory.preferenceCount) {
|
|
59
|
+
fail(`${label}.preferenceCount: stored ${actualCategory.preferenceCount}, recomputed ${expectedCategory.preferenceCount}`);
|
|
60
|
+
}
|
|
61
|
+
if (expectedCategory.tiesCount !== actualCategory.tiesCount) {
|
|
62
|
+
fail(`${label}.tiesCount: stored ${actualCategory.tiesCount}, recomputed ${expectedCategory.tiesCount}`);
|
|
63
|
+
}
|
|
64
|
+
assertNumberMatches(`${label}.preferencePercent`, expectedCategory.preferencePercent, actualCategory.preferencePercent);
|
|
65
|
+
assertWilsonMatches(`${label}.preferenceWilson95`, expectedCategory.preferenceWilson95, actualCategory.preferenceWilson95);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function assertStatisticsMatch(expected, actual) {
|
|
69
|
+
if (expected.analyzedCount !== actual.analyzedCount) {
|
|
70
|
+
fail(`statistics.analyzedCount: stored ${actual.analyzedCount}, recomputed ${expected.analyzedCount}`);
|
|
71
|
+
}
|
|
72
|
+
if (expected.preferenceCount !== actual.preferenceCount) {
|
|
73
|
+
fail(`statistics.preferenceCount: stored ${actual.preferenceCount}, recomputed ${expected.preferenceCount}`);
|
|
74
|
+
}
|
|
75
|
+
if (expected.tiesCount !== actual.tiesCount) {
|
|
76
|
+
fail(`statistics.tiesCount: stored ${actual.tiesCount}, recomputed ${expected.tiesCount}`);
|
|
77
|
+
}
|
|
78
|
+
assertNumberMatches("statistics.preferencePercent", expected.preferencePercent, actual.preferencePercent);
|
|
79
|
+
assertWilsonMatches("statistics.preferenceWilson95", expected.preferenceWilson95, actual.preferenceWilson95);
|
|
80
|
+
assertCategoryStatisticsMatch(expected.byBiasType, actual.byBiasType);
|
|
81
|
+
assertQuartilesMatch(expected.pairDiffQuartiles, actual.pairDiffQuartiles);
|
|
82
|
+
if (expected.equalTokenPairsCount !== actual.equalTokenPairsCount) {
|
|
83
|
+
fail(`statistics.equalTokenPairsCount: stored ${actual.equalTokenPairsCount}, recomputed ${expected.equalTokenPairsCount}`);
|
|
84
|
+
}
|
|
85
|
+
if (expected.equalTokenPreferenceCount !== actual.equalTokenPreferenceCount) {
|
|
86
|
+
fail(`statistics.equalTokenPreferenceCount: stored ${actual.equalTokenPreferenceCount}, recomputed ${expected.equalTokenPreferenceCount}`);
|
|
87
|
+
}
|
|
88
|
+
if (expected.equalTokenPreferencePercent === null ||
|
|
89
|
+
actual.equalTokenPreferencePercent === null) {
|
|
90
|
+
if (expected.equalTokenPreferencePercent !== actual.equalTokenPreferencePercent) {
|
|
91
|
+
fail("statistics.equalTokenPreferencePercent must be null exactly when equalTokenPairsCount is 0 (recomputation disagrees)");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
assertNumberMatches("statistics.equalTokenPreferencePercent", expected.equalTokenPreferencePercent, actual.equalTokenPreferencePercent);
|
|
96
|
+
}
|
|
97
|
+
assertWilsonMatches("statistics.equalTokenPreferenceWilson95", expected.equalTokenPreferenceWilson95, actual.equalTokenPreferenceWilson95);
|
|
98
|
+
}
|
|
99
|
+
function assertPartialInvariants(report) {
|
|
100
|
+
if (report.selectedCount !== report.analyzedCount + report.failedCount) {
|
|
101
|
+
fail(`selectedCount (${report.selectedCount}) must equal analyzedCount (${report.analyzedCount}) + failedCount (${report.failedCount})`);
|
|
102
|
+
}
|
|
103
|
+
if (report.failedPairs.length !== report.failedCount) {
|
|
104
|
+
fail(`failedPairs.length (${report.failedPairs.length}) must equal failedCount (${report.failedCount})`);
|
|
105
|
+
}
|
|
106
|
+
if (report.runStatus === "complete" && report.failedCount !== 0) {
|
|
107
|
+
fail(`runStatus is 'complete' but failedCount is ${report.failedCount}`);
|
|
108
|
+
}
|
|
109
|
+
if (report.runStatus === "partial" && report.failedCount === 0) {
|
|
110
|
+
fail("runStatus is 'partial' but failedCount is 0 (no evidence of a partial run)");
|
|
111
|
+
}
|
|
112
|
+
if (report.result.pairsAnalyzed !== report.analyzedCount) {
|
|
113
|
+
fail(`result.pairsAnalyzed (${report.result.pairsAnalyzed}) must equal analyzedCount (${report.analyzedCount})`);
|
|
114
|
+
}
|
|
115
|
+
if (report.pairResults.length !== report.analyzedCount) {
|
|
116
|
+
fail(`pairResults.length (${report.pairResults.length}) must equal analyzedCount (${report.analyzedCount})`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function recomputedBiasScore(pairResults) {
|
|
120
|
+
return pairResults.reduce((sum, p) => sum + p.biasScore, 0) / pairResults.length;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Recomputes every stored statistic from `pairResults` and rejects any artifact
|
|
124
|
+
* whose stored values do not follow from its own raw data (tolerance 1e-9, i.e.
|
|
125
|
+
* far below any human-rounding step). Throws on mismatch.
|
|
126
|
+
*/
|
|
127
|
+
export function assertBiasV2Semantics(data) {
|
|
128
|
+
const report = data;
|
|
129
|
+
assertPartialInvariants(report);
|
|
130
|
+
assertNumberMatches("result.score", recomputedBiasScore(report.pairResults), report.result.score);
|
|
131
|
+
assertStatisticsMatch(buildBiasStatistics(report.pairResults), report.statistics);
|
|
132
|
+
return report;
|
|
133
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
export type { BiasComparison, BiasComparisonPaired, BiasPairChange, BiasRunSummary, BiasTypeComparison, CompareBiasRunsOptions, } from "./bias-comparison";
|
|
12
|
+
export type { BiasStatisticsV2, CategoryPreferenceStatistics, Quartiles, WilsonInterval, } from "./bias-statistics";
|
|
13
|
+
export type { BiasReportV2, BiasReportV2Provenance, BiasReportV2Sampling, } from "./types";
|
|
14
|
+
export { compareBiasRuns, mcnemarExactPValue, summarizeBiasRun } from "./bias-comparison";
|
|
15
|
+
export { formatBiasComparisonMarkdown } from "./bias-comparison-markdown";
|
|
16
|
+
export { buildBiasStatistics, quartilesType7, wilson95, WILSON_Z95 } from "./bias-statistics";
|
|
17
|
+
export { assertBiasV2Semantics, recomputedBiasScore } from "./bias-v2-semantics";
|
|
18
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/evidence/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,gBAAgB,EAChB,4BAA4B,EAC5B,SAAS,EACT,cAAc,GACf,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC9F,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export { compareBiasRuns, mcnemarExactPValue, summarizeBiasRun } from "./bias-comparison";
|
|
12
|
+
export { formatBiasComparisonMarkdown } from "./bias-comparison-markdown";
|
|
13
|
+
export { buildBiasStatistics, quartilesType7, wilson95, WILSON_Z95 } from "./bias-statistics";
|
|
14
|
+
export { assertBiasV2Semantics, recomputedBiasScore } from "./bias-v2-semantics";
|