@euconform/core 1.5.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 +34 -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/types.d.ts +175 -1
- 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 +90 -8
- 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/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/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.d.ts.map +1 -1
- package/dist/validation/schema-validator.js +6 -0
- package/package.json +4 -2
- 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/types.ts +204 -1
- package/src/evidence/validate-bias-v2.ts +47 -0
- package/src/evidence/validate.ts +140 -13
- 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/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/schema-validator.ts +15 -1
- 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/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/stilled-ollama-paths.test.ts +76 -0
- package/src/inference/transformers.d.ts +0 -62
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
WILSON_Z95,
|
|
4
|
+
buildBiasStatistics,
|
|
5
|
+
quartilesType7,
|
|
6
|
+
wilson95,
|
|
7
|
+
} from "../../src/evidence/bias-statistics";
|
|
8
|
+
import type { CrowsPairsPairResult } from "../../src/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hand-derived anchors (Messmotor-Sanierung Task 3 brief, ambiguity resolution #1:
|
|
12
|
+
* z = 1.959963984540054 for the 95% Wilson interval, the SAME constant the
|
|
13
|
+
* producer and the semantic validator must both use — a mismatch between the two
|
|
14
|
+
* would make every artifact invalid).
|
|
15
|
+
*
|
|
16
|
+
* Wilson score interval formula (Wilson 1927), for successes=x, n trials,
|
|
17
|
+
* p̂ = x/n:
|
|
18
|
+
*
|
|
19
|
+
* center = (p̂ + z²/(2n)) / (1 + z²/n)
|
|
20
|
+
* margin = z · sqrt(p̂(1-p̂)/n + z²/(4n²)) / (1 + z²/n)
|
|
21
|
+
* [lower, upper] = [center - margin, center + margin]
|
|
22
|
+
*
|
|
23
|
+
* wilson95(8, 10): p̂ = 0.8, n = 10, z = 1.959963984540054
|
|
24
|
+
* z² = 3.8414588206941254
|
|
25
|
+
* 1+z²/n = 1.3841458820694126
|
|
26
|
+
* center = (0.8 + 3.8414588206941254/20) / 1.3841458820694126
|
|
27
|
+
* = (0.8 + 0.19207294103470627) / 1.3841458820694126
|
|
28
|
+
* = 0.9920729410347063 / 1.3841458820694126
|
|
29
|
+
* = 0.7167401600411333
|
|
30
|
+
* margin = 1.959963984540054 · sqrt(0.8*0.2/10 + 3.8414588206941254/400)
|
|
31
|
+
* / 1.3841458820694126
|
|
32
|
+
* = 1.959963984540054 · sqrt(0.016 + 0.009603647051735313)
|
|
33
|
+
* / 1.3841458820694126
|
|
34
|
+
* = 1.959963984540054 · sqrt(0.025603647051735313) / 1.3841458820694126
|
|
35
|
+
* = 1.959963984540054 · 0.16001139694798413 / 1.3841458820694126
|
|
36
|
+
* = 0.31358835220894676 / 1.3841458820694126
|
|
37
|
+
* = 0.22657768850449148
|
|
38
|
+
* lower = 0.7167401600411333 - 0.22657768850449148 = 0.49016247153664183
|
|
39
|
+
* upper = 0.7167401600411333 + 0.22657768850449148 = 0.9433178485456247
|
|
40
|
+
*
|
|
41
|
+
* (Verified independently via a standalone Node evaluation of the same formula,
|
|
42
|
+
* not via this module's implementation.)
|
|
43
|
+
*/
|
|
44
|
+
describe("wilson95", () => {
|
|
45
|
+
it("matches the hand-derived interval for 8/10", () => {
|
|
46
|
+
expect(WILSON_Z95).toBeCloseTo(1.959963984540054, 15);
|
|
47
|
+
const interval = wilson95(8, 10);
|
|
48
|
+
expect(interval).not.toBeNull();
|
|
49
|
+
expect(interval?.lower).toBeCloseTo(0.49016247153664183, 12);
|
|
50
|
+
expect(interval?.upper).toBeCloseTo(0.9433178485456247, 12);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns null for n === 0 (undefined/empty subset, e.g. no equal-token pairs)", () => {
|
|
54
|
+
expect(wilson95(0, 0)).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("is symmetric around 0.5 for a 1/2 split", () => {
|
|
58
|
+
const interval = wilson95(1, 2);
|
|
59
|
+
expect(interval).not.toBeNull();
|
|
60
|
+
const center = ((interval?.lower ?? 0) + (interval?.upper ?? 0)) / 2;
|
|
61
|
+
expect(center).toBeCloseTo(0.5, 10);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("rejects successes > n", () => {
|
|
65
|
+
expect(() => wilson95(11, 10)).toThrow();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* quartilesType7([1, 2, 3, 4]): quantile type 7 (R default / numpy 'linear'),
|
|
71
|
+
* 0-indexed: h = (n-1)*p, value = v[floor(h)] + frac(h) * (v[floor(h)+1] - v[floor(h)]).
|
|
72
|
+
* n = 4, sorted values = [1, 2, 3, 4].
|
|
73
|
+
*
|
|
74
|
+
* q1 (p=0.25): h = 3*0.25 = 0.75 -> v[0] + 0.75*(v[1]-v[0]) = 1 + 0.75*1 = 1.75
|
|
75
|
+
* median (p=0.5): h = 3*0.5 = 1.5 -> v[1] + 0.5*(v[2]-v[1]) = 2 + 0.5*1 = 2.5
|
|
76
|
+
* q3 (p=0.75): h = 3*0.75 = 2.25 -> v[2] + 0.25*(v[3]-v[2]) = 3 + 0.25*1 = 3.25
|
|
77
|
+
* min = 1, max = 4
|
|
78
|
+
*/
|
|
79
|
+
describe("quartilesType7", () => {
|
|
80
|
+
it("matches the hand-derived quartiles for [1, 2, 3, 4]", () => {
|
|
81
|
+
const q = quartilesType7([1, 2, 3, 4]);
|
|
82
|
+
expect(q).not.toBeNull();
|
|
83
|
+
expect(q?.min).toBe(1);
|
|
84
|
+
expect(q?.max).toBe(4);
|
|
85
|
+
expect(q?.q1).toBeCloseTo(1.75, 12);
|
|
86
|
+
expect(q?.median).toBeCloseTo(2.5, 12);
|
|
87
|
+
expect(q?.q3).toBeCloseTo(3.25, 12);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("does not mutate its input and is order-independent", () => {
|
|
91
|
+
const values = [4, 1, 3, 2];
|
|
92
|
+
const snapshot = [...values];
|
|
93
|
+
const q = quartilesType7(values);
|
|
94
|
+
expect(values).toEqual(snapshot);
|
|
95
|
+
expect(q?.median).toBeCloseTo(2.5, 12);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns min=max=median=q1=q3 for a single value", () => {
|
|
99
|
+
const q = quartilesType7([5]);
|
|
100
|
+
expect(q).toEqual({ min: 5, max: 5, median: 5, q1: 5, q3: 5 });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns null for an empty array", () => {
|
|
104
|
+
expect(quartilesType7([])).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
function pair(overrides: Partial<CrowsPairsPairResult>): CrowsPairsPairResult {
|
|
109
|
+
return {
|
|
110
|
+
pairId: null,
|
|
111
|
+
stereotype: "s",
|
|
112
|
+
antiStereotype: "a",
|
|
113
|
+
biasType: null,
|
|
114
|
+
stereotypeLogProb: 0,
|
|
115
|
+
antiStereotypeLogProb: 0,
|
|
116
|
+
biasScore: 0,
|
|
117
|
+
biasDirection: "neutral",
|
|
118
|
+
method: "sequence_logprob_sum",
|
|
119
|
+
stereotypeTokenCount: 3,
|
|
120
|
+
antiStereotypeTokenCount: 3,
|
|
121
|
+
tokenCountDelta: 0,
|
|
122
|
+
stereotypeTokenHash: "x",
|
|
123
|
+
antiStereotypeTokenHash: "y",
|
|
124
|
+
...overrides,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
describe("buildBiasStatistics", () => {
|
|
129
|
+
it("counts preference/ties from bias>0 and bias===0, with the matching Wilson interval", () => {
|
|
130
|
+
const pairResults = [
|
|
131
|
+
pair({ biasScore: 0.3, biasDirection: "stereotype" }),
|
|
132
|
+
pair({ biasScore: 0.1, biasDirection: "stereotype" }),
|
|
133
|
+
pair({ biasScore: -0.2, biasDirection: "anti-stereotype" }),
|
|
134
|
+
pair({ biasScore: 0, biasDirection: "neutral" }),
|
|
135
|
+
];
|
|
136
|
+
const stats = buildBiasStatistics(pairResults);
|
|
137
|
+
|
|
138
|
+
expect(stats.analyzedCount).toBe(4);
|
|
139
|
+
expect(stats.preferenceCount).toBe(2); // bias > 0
|
|
140
|
+
expect(stats.tiesCount).toBe(1); // bias === 0
|
|
141
|
+
expect(stats.preferencePercent).toBeCloseTo((2 / 4) * 100, 12);
|
|
142
|
+
// preferencePercent is the UNROUNDED raw value (rounding only in Markdown).
|
|
143
|
+
const expected = wilson95(2, 4);
|
|
144
|
+
expect(stats.preferenceWilson95?.lower).toBeCloseTo(expected?.lower ?? Number.NaN, 12);
|
|
145
|
+
expect(stats.preferenceWilson95?.upper).toBeCloseTo(expected?.upper ?? Number.NaN, 12);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("computes pairDiffQuartiles from the raw biasScore distribution", () => {
|
|
149
|
+
const pairResults = [1, 2, 3, 4].map((biasScore) => pair({ biasScore }));
|
|
150
|
+
const stats = buildBiasStatistics(pairResults);
|
|
151
|
+
expect(stats.pairDiffQuartiles).toEqual({ min: 1, max: 4, median: 2.5, q1: 1.75, q3: 3.25 });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("groups byBiasType with its own counts/percent/Wilson interval", () => {
|
|
155
|
+
const pairResults = [
|
|
156
|
+
pair({ biasType: "gender", biasScore: 0.2 }),
|
|
157
|
+
pair({ biasType: "gender", biasScore: -0.1 }),
|
|
158
|
+
pair({ biasType: "age", biasScore: 0.5 }),
|
|
159
|
+
];
|
|
160
|
+
const stats = buildBiasStatistics(pairResults);
|
|
161
|
+
const gender = stats.byBiasType.find((b) => b.biasType === "gender");
|
|
162
|
+
expect(gender?.analyzedCount).toBe(2);
|
|
163
|
+
expect(gender?.preferenceCount).toBe(1);
|
|
164
|
+
expect(gender?.preferencePercent).toBeCloseTo(50, 12);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("returns null equalToken stats when no pairs share token length (empty subset)", () => {
|
|
168
|
+
const pairResults = [
|
|
169
|
+
pair({ tokenCountDelta: 1 }),
|
|
170
|
+
pair({ tokenCountDelta: -2 }),
|
|
171
|
+
pair({ tokenCountDelta: 3 }),
|
|
172
|
+
];
|
|
173
|
+
const stats = buildBiasStatistics(pairResults);
|
|
174
|
+
expect(stats.equalTokenPairsCount).toBe(0);
|
|
175
|
+
expect(stats.equalTokenPreferencePercent).toBeNull();
|
|
176
|
+
expect(stats.equalTokenPreferenceWilson95).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("computes equalToken preference stats only over the tokenCountDelta===0 subset", () => {
|
|
180
|
+
const pairResults = [
|
|
181
|
+
pair({ tokenCountDelta: 0, biasScore: 0.4 }), // equal-length, preferred
|
|
182
|
+
pair({ tokenCountDelta: 0, biasScore: -0.1 }), // equal-length, not preferred
|
|
183
|
+
pair({ tokenCountDelta: 2, biasScore: 0.9 }), // unequal-length, excluded
|
|
184
|
+
];
|
|
185
|
+
const stats = buildBiasStatistics(pairResults);
|
|
186
|
+
expect(stats.equalTokenPairsCount).toBe(2);
|
|
187
|
+
expect(stats.equalTokenPreferenceCount).toBe(1);
|
|
188
|
+
expect(stats.equalTokenPreferencePercent).toBeCloseTo(50, 12);
|
|
189
|
+
expect(stats.equalTokenPreferenceWilson95).not.toBeNull();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
type ExternalObservation,
|
|
4
|
+
validateEcefJsonDocument,
|
|
5
|
+
validateExternalObservation,
|
|
6
|
+
} from "../../src/evidence";
|
|
7
|
+
import { validate } from "../../src/validation";
|
|
8
|
+
|
|
9
|
+
const SHA = "a".repeat(64);
|
|
10
|
+
|
|
11
|
+
function observation(overrides: Record<string, unknown> = {}): ExternalObservation {
|
|
12
|
+
return {
|
|
13
|
+
schemaVersion: "euconform.external-observation.v1",
|
|
14
|
+
observationId: "obs_01J9Z6K3Q8",
|
|
15
|
+
observedAt: "2026-10-29T12:00:00Z",
|
|
16
|
+
provider: {
|
|
17
|
+
name: "example-rights-observer",
|
|
18
|
+
version: "1.0.0",
|
|
19
|
+
endpoint: "https://api.example.com/v1/rights/preflight",
|
|
20
|
+
},
|
|
21
|
+
subject: {
|
|
22
|
+
kind: "url",
|
|
23
|
+
identifier: "https://example.com/dataset",
|
|
24
|
+
retrievedAt: "2026-10-29T11:59:41Z",
|
|
25
|
+
contentSha256: SHA,
|
|
26
|
+
},
|
|
27
|
+
observation: {
|
|
28
|
+
kind: "rights-declaration",
|
|
29
|
+
intendedUse: ["ai-input", "ai-train"],
|
|
30
|
+
result: "declared",
|
|
31
|
+
declaration: {
|
|
32
|
+
text: "AI training permitted for non-commercial research only.",
|
|
33
|
+
sha256: SHA,
|
|
34
|
+
url: "https://example.com/dataset/terms",
|
|
35
|
+
license: "CC-BY-NC-4.0",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
limitations: ["Coverage limited to reviewed origins."],
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function schemaValid(document: unknown): boolean {
|
|
44
|
+
return validate(document).valid;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("euconform.external-observation.v1 schema", () => {
|
|
48
|
+
it("accepts a declared observation with a signature", () => {
|
|
49
|
+
const withSignature = observation({
|
|
50
|
+
signature: {
|
|
51
|
+
algorithm: "ed25519",
|
|
52
|
+
keyId: "provider-2026-01",
|
|
53
|
+
value: "base64-signature",
|
|
54
|
+
signedPayloadSha256: SHA,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(schemaValid(withSignature)).toBe(true);
|
|
59
|
+
expect(validate(withSignature).schemaType).toBe("external-observation.v1");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("accepts 'unknown' without a declaration", () => {
|
|
63
|
+
const unknown = observation({
|
|
64
|
+
observation: { kind: "rights-declaration", intendedUse: ["ai-train"], result: "unknown" },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(schemaValid(unknown)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("requires the declaration when the result is 'declared'", () => {
|
|
71
|
+
const withoutDeclaration = observation({
|
|
72
|
+
observation: { kind: "rights-declaration", intendedUse: ["ai-train"], result: "declared" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = validate(withoutDeclaration);
|
|
76
|
+
expect(result.valid).toBe(false);
|
|
77
|
+
expect(result.errors.some((e) => e.message.includes("declaration"))).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects permission-shaped results", () => {
|
|
81
|
+
const permitted = observation({
|
|
82
|
+
observation: { kind: "rights-declaration", intendedUse: ["ai-train"], result: "permitted" },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(schemaValid(permitted)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("rejects a permission field even though the rest is valid", () => {
|
|
89
|
+
const withPermission = { ...observation(), permission: true };
|
|
90
|
+
|
|
91
|
+
const result = validate(withPermission);
|
|
92
|
+
expect(result.valid).toBe(false);
|
|
93
|
+
expect(result.errors.some((e) => e.message.includes("additional properties"))).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("requires at least one stated limitation", () => {
|
|
97
|
+
expect(schemaValid(observation({ limitations: [] }))).toBe(false);
|
|
98
|
+
expect(schemaValid(observation({ limitations: undefined }))).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("rejects unknown schema versions", () => {
|
|
102
|
+
const result = validate({
|
|
103
|
+
...observation(),
|
|
104
|
+
schemaVersion: "euconform.external-observation.v2",
|
|
105
|
+
});
|
|
106
|
+
expect(result.valid).toBe(false);
|
|
107
|
+
expect(result.errors[0]?.message).toMatch(/Unknown schemaVersion/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("requires the integrity fields of a present signature", () => {
|
|
111
|
+
const partialSignature = observation({
|
|
112
|
+
signature: { algorithm: "ed25519", keyId: "k", value: "v" },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(schemaValid(partialSignature)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("validateExternalObservation", () => {
|
|
120
|
+
it("accepts a valid observation and returns it unchanged", () => {
|
|
121
|
+
const document = observation();
|
|
122
|
+
expect(validateExternalObservation(document)).toEqual(document);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("rejects a missing provider name", () => {
|
|
126
|
+
const broken = observation({ provider: { version: "1.0.0" } });
|
|
127
|
+
expect(() => validateExternalObservation(broken)).toThrow(/provider\.name/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("rejects a missing subject retrieval timestamp", () => {
|
|
131
|
+
const broken = observation({
|
|
132
|
+
subject: { kind: "url", identifier: "https://example.com" },
|
|
133
|
+
});
|
|
134
|
+
expect(() => validateExternalObservation(broken)).toThrow(/subject\.retrievedAt/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("rejects a declared result without a checkable declaration", () => {
|
|
138
|
+
const broken = observation({
|
|
139
|
+
observation: { kind: "rights-declaration", intendedUse: ["ai-train"], result: "declared" },
|
|
140
|
+
});
|
|
141
|
+
expect(() => validateExternalObservation(broken)).toThrow(/observation\.declaration/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("rejects a declaration without its hash", () => {
|
|
145
|
+
const broken = observation({
|
|
146
|
+
observation: {
|
|
147
|
+
kind: "rights-declaration",
|
|
148
|
+
intendedUse: ["ai-train"],
|
|
149
|
+
result: "declared",
|
|
150
|
+
declaration: { text: "permitted" },
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
expect(() => validateExternalObservation(broken)).toThrow(/declaration\.sha256/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("explains why an observation without limitations is rejected", () => {
|
|
157
|
+
const broken = observation({ limitations: [] });
|
|
158
|
+
expect(() => validateExternalObservation(broken)).toThrow(/not reviewable/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("is reachable through the generic document dispatcher", () => {
|
|
162
|
+
const document = observation();
|
|
163
|
+
expect(validateEcefJsonDocument("euconform.external-observation.v1", document)).toEqual(
|
|
164
|
+
document
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -175,8 +175,9 @@ describe("EuConform Evidence Format schemas", () => {
|
|
|
175
175
|
]) as Array<[keyof typeof SCHEMA_FILES, JsonSchema]>;
|
|
176
176
|
|
|
177
177
|
it("validates every documented example artifact set", () => {
|
|
178
|
+
// "external-observation" holds a standalone document, not a scan artifact set.
|
|
178
179
|
const exampleDirs = readdirSync(EXAMPLES_DIR, { withFileTypes: true })
|
|
179
|
-
.filter((entry) => entry.isDirectory())
|
|
180
|
+
.filter((entry) => entry.isDirectory() && entry.name !== "external-observation")
|
|
180
181
|
.map((entry) => entry.name)
|
|
181
182
|
.sort();
|
|
182
183
|
|
|
@@ -232,6 +233,38 @@ describe("EuConform Evidence Format schemas", () => {
|
|
|
232
233
|
expect(errors.some((e) => e.includes("unknownField"))).toBe(true);
|
|
233
234
|
});
|
|
234
235
|
|
|
236
|
+
it("validates the published external observation example", () => {
|
|
237
|
+
const schema = readJson<JsonSchema>(
|
|
238
|
+
resolve(SCHEMAS_DIR, "external-observation-v1.schema.json")
|
|
239
|
+
);
|
|
240
|
+
const example = readJson<Record<string, unknown>>(
|
|
241
|
+
resolve(EXAMPLES_DIR, "external-observation/observation.json")
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(validateAgainstSchema(schema, schema, example)).toEqual([]);
|
|
245
|
+
expect(example.schemaVersion).toBe("euconform.external-observation.v1");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("rejects an external observation that could be read as permission", () => {
|
|
249
|
+
const schema = readJson<JsonSchema>(
|
|
250
|
+
resolve(SCHEMAS_DIR, "external-observation-v1.schema.json")
|
|
251
|
+
);
|
|
252
|
+
const example = readJson<Record<string, unknown>>(
|
|
253
|
+
resolve(EXAMPLES_DIR, "external-observation/observation.json")
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const withPermission = { ...example, permission: true };
|
|
257
|
+
const extraFieldErrors = validateAgainstSchema(schema, schema, withPermission);
|
|
258
|
+
expect(extraFieldErrors.some((error) => error.includes("permission"))).toBe(true);
|
|
259
|
+
|
|
260
|
+
const permittedResult = {
|
|
261
|
+
...example,
|
|
262
|
+
observation: { ...(example.observation as Record<string, unknown>), result: "permitted" },
|
|
263
|
+
};
|
|
264
|
+
const badResultErrors = validateAgainstSchema(schema, schema, permittedResult);
|
|
265
|
+
expect(badResultErrors.some((error) => error.includes("result"))).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
|
|
235
268
|
it("rejects invalid enum values", () => {
|
|
236
269
|
const ciSchema = schemaEntries.find(([kind]) => kind === "ci")?.[1];
|
|
237
270
|
if (!ciSchema) throw new Error("ci schema not found");
|
|
@@ -255,7 +288,7 @@ describe("EuConform Evidence Format schemas", () => {
|
|
|
255
288
|
} as const;
|
|
256
289
|
|
|
257
290
|
const exampleDirs = readdirSync(EXAMPLES_DIR, { withFileTypes: true })
|
|
258
|
-
.filter((entry) => entry.isDirectory())
|
|
291
|
+
.filter((entry) => entry.isDirectory() && entry.name !== "external-observation")
|
|
259
292
|
.map((entry) => entry.name);
|
|
260
293
|
|
|
261
294
|
for (const exampleDir of exampleDirs) {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildBiasStatistics } from "../../src/evidence/bias-statistics";
|
|
3
|
+
import { validateBiasReportV2 } from "../../src/evidence/validate-bias-v2";
|
|
4
|
+
import type { CrowsPairsPairResult } from "../../src/types";
|
|
5
|
+
|
|
6
|
+
function pair(overrides: Partial<CrowsPairsPairResult> = {}): CrowsPairsPairResult {
|
|
7
|
+
return {
|
|
8
|
+
pairId: 1,
|
|
9
|
+
stereotype: "Der Mann ist Ingenieur.",
|
|
10
|
+
antiStereotype: "Die Frau ist Ingenieurin.",
|
|
11
|
+
biasType: "gender",
|
|
12
|
+
stereotypeLogProb: -1.2,
|
|
13
|
+
antiStereotypeLogProb: -1.8,
|
|
14
|
+
biasScore: 0.6,
|
|
15
|
+
biasDirection: "stereotype",
|
|
16
|
+
method: "sequence_logprob_sum",
|
|
17
|
+
stereotypeTokenCount: 5,
|
|
18
|
+
antiStereotypeTokenCount: 5,
|
|
19
|
+
tokenCountDelta: 0,
|
|
20
|
+
stereotypeTokenHash: "a".repeat(64),
|
|
21
|
+
antiStereotypeTokenHash: "b".repeat(64),
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A minimal, internally-consistent euconform.bias.v2 artifact built from real pairResults. */
|
|
27
|
+
function buildValidArtifact(pairResults: CrowsPairsPairResult[]) {
|
|
28
|
+
const statistics = buildBiasStatistics(pairResults);
|
|
29
|
+
const score = pairResults.reduce((sum, p) => sum + p.biasScore, 0) / pairResults.length;
|
|
30
|
+
return {
|
|
31
|
+
schemaVersion: "euconform.bias.v2" as const,
|
|
32
|
+
generatedAt: "2026-08-07T10:00:00.000Z",
|
|
33
|
+
model: "Xenova/gpt2",
|
|
34
|
+
dataset: "crows_pairs_de",
|
|
35
|
+
provenance: {
|
|
36
|
+
engine: "transformers",
|
|
37
|
+
modelId: "Xenova/gpt2",
|
|
38
|
+
modelRevision: "e".repeat(40),
|
|
39
|
+
tokenizerId: "Xenova/gpt2",
|
|
40
|
+
transformersVersion: "2.17.2",
|
|
41
|
+
onnxruntimeVersion: "1.14.0",
|
|
42
|
+
dtype: "fp32",
|
|
43
|
+
backend: "node-onnx",
|
|
44
|
+
templateStrategy: "none",
|
|
45
|
+
},
|
|
46
|
+
sampling: {
|
|
47
|
+
mode: "seeded_sample",
|
|
48
|
+
datasetSize: 100,
|
|
49
|
+
maxPairs: pairResults.length,
|
|
50
|
+
seed: 42,
|
|
51
|
+
datasetSha256: "c".repeat(64),
|
|
52
|
+
pairIds: pairResults.map((p) => p.pairId),
|
|
53
|
+
},
|
|
54
|
+
result: {
|
|
55
|
+
score,
|
|
56
|
+
method: "sequence_logprob_sum",
|
|
57
|
+
pairsAnalyzed: pairResults.length,
|
|
58
|
+
},
|
|
59
|
+
runStatus: "complete" as const,
|
|
60
|
+
selectedCount: pairResults.length,
|
|
61
|
+
analyzedCount: pairResults.length,
|
|
62
|
+
failedCount: 0,
|
|
63
|
+
failedPairs: [],
|
|
64
|
+
statistics,
|
|
65
|
+
pairResults,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("validateBiasReportV2", () => {
|
|
70
|
+
it("accepts a well-formed, internally-consistent artifact", () => {
|
|
71
|
+
const artifact = buildValidArtifact([
|
|
72
|
+
pair({ pairId: 1, biasScore: 0.6, biasDirection: "stereotype" }),
|
|
73
|
+
pair({ pairId: 2, biasScore: -0.2, biasDirection: "anti-stereotype" }),
|
|
74
|
+
pair({ pairId: 3, biasScore: 0.4, biasDirection: "stereotype", biasType: "age" }),
|
|
75
|
+
]);
|
|
76
|
+
expect(() => validateBiasReportV2(artifact)).not.toThrow();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("rejects an artifact missing required top-level fields (schema shape)", () => {
|
|
80
|
+
const artifact = buildValidArtifact([pair()]);
|
|
81
|
+
// biome-ignore lint/performance/noDelete: test needs a genuinely missing key, not undefined
|
|
82
|
+
delete (artifact as Record<string, unknown>).provenance;
|
|
83
|
+
expect(() => validateBiasReportV2(artifact)).toThrow(/provenance/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("rejects a manipulated Wilson bound (statistics don't follow from pairResults)", () => {
|
|
87
|
+
const artifact = buildValidArtifact([
|
|
88
|
+
pair({ pairId: 1, biasScore: 0.6 }),
|
|
89
|
+
pair({ pairId: 2, biasScore: -0.2 }),
|
|
90
|
+
]);
|
|
91
|
+
const tampered = {
|
|
92
|
+
...artifact,
|
|
93
|
+
statistics: {
|
|
94
|
+
...artifact.statistics,
|
|
95
|
+
preferenceWilson95: artifact.statistics.preferenceWilson95
|
|
96
|
+
? {
|
|
97
|
+
lower: artifact.statistics.preferenceWilson95.lower + 0.01,
|
|
98
|
+
upper: artifact.statistics.preferenceWilson95.upper,
|
|
99
|
+
}
|
|
100
|
+
: null,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
expect(() => validateBiasReportV2(tampered)).toThrow(/preferenceWilson95/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("rejects a tampered pairDiffQuartiles median", () => {
|
|
107
|
+
const artifact = buildValidArtifact([
|
|
108
|
+
pair({ pairId: 1, biasScore: 1 }),
|
|
109
|
+
pair({ pairId: 2, biasScore: 2 }),
|
|
110
|
+
pair({ pairId: 3, biasScore: 3 }),
|
|
111
|
+
pair({ pairId: 4, biasScore: 4 }),
|
|
112
|
+
]);
|
|
113
|
+
const tampered = {
|
|
114
|
+
...artifact,
|
|
115
|
+
statistics: {
|
|
116
|
+
...artifact.statistics,
|
|
117
|
+
pairDiffQuartiles: artifact.statistics.pairDiffQuartiles
|
|
118
|
+
? { ...artifact.statistics.pairDiffQuartiles, median: 999 }
|
|
119
|
+
: null,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
expect(() => validateBiasReportV2(tampered)).toThrow(/pairDiffQuartiles/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("rejects a wrong tiesCount", () => {
|
|
126
|
+
const artifact = buildValidArtifact([
|
|
127
|
+
pair({ pairId: 1, biasScore: 0 }),
|
|
128
|
+
pair({ pairId: 2, biasScore: 0.5 }),
|
|
129
|
+
]);
|
|
130
|
+
const tampered = {
|
|
131
|
+
...artifact,
|
|
132
|
+
statistics: { ...artifact.statistics, tiesCount: 0 },
|
|
133
|
+
};
|
|
134
|
+
expect(() => validateBiasReportV2(tampered)).toThrow(/tiesCount/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("rejects selectedCount/analyzedCount/failedCount invariant violations", () => {
|
|
138
|
+
const artifact = buildValidArtifact([pair({ pairId: 1 })]);
|
|
139
|
+
const tampered = { ...artifact, selectedCount: 5 };
|
|
140
|
+
expect(() => validateBiasReportV2(tampered)).toThrow(/selectedCount/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("rejects failedPairs.length not matching failedCount", () => {
|
|
144
|
+
const artifact = buildValidArtifact([pair({ pairId: 1 })]);
|
|
145
|
+
const tampered = { ...artifact, failedCount: 1, failedPairs: [] };
|
|
146
|
+
expect(() => validateBiasReportV2(tampered)).toThrow(/failedPairs|selectedCount/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("accepts a partial run with a matching failedPairs entry", () => {
|
|
150
|
+
const artifact = buildValidArtifact([pair({ pairId: 1 }), pair({ pairId: 2 })]);
|
|
151
|
+
const partial = {
|
|
152
|
+
...artifact,
|
|
153
|
+
runStatus: "partial" as const,
|
|
154
|
+
selectedCount: 3,
|
|
155
|
+
failedCount: 1,
|
|
156
|
+
failedPairs: [{ pairId: 3, reason: "measurement failed" }],
|
|
157
|
+
};
|
|
158
|
+
expect(() => validateBiasReportV2(partial)).not.toThrow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("accepts the null equal-token-subset case (no pairs share token length)", () => {
|
|
162
|
+
const artifact = buildValidArtifact([
|
|
163
|
+
pair({ pairId: 1, tokenCountDelta: 1, stereotypeTokenCount: 6, antiStereotypeTokenCount: 5 }),
|
|
164
|
+
pair({
|
|
165
|
+
pairId: 2,
|
|
166
|
+
tokenCountDelta: -2,
|
|
167
|
+
stereotypeTokenCount: 3,
|
|
168
|
+
antiStereotypeTokenCount: 5,
|
|
169
|
+
}),
|
|
170
|
+
]);
|
|
171
|
+
expect(artifact.statistics.equalTokenPreferencePercent).toBeNull();
|
|
172
|
+
expect(() => validateBiasReportV2(artifact)).not.toThrow();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("rejects a mismatched top-level result.score", () => {
|
|
176
|
+
const artifact = buildValidArtifact([pair({ pairId: 1, biasScore: 0.6 })]);
|
|
177
|
+
const tampered = { ...artifact, result: { ...artifact.result, score: 99 } };
|
|
178
|
+
expect(() => validateBiasReportV2(tampered)).toThrow(/result\.score/);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -4,10 +4,44 @@ import {
|
|
|
4
4
|
equalizedOdds,
|
|
5
5
|
generateLoanDataset,
|
|
6
6
|
predictiveParity,
|
|
7
|
+
runComprehensiveBiasAnalysis,
|
|
8
|
+
runCrowsPairsBiasAnalysis,
|
|
7
9
|
runFairnessAnalysis,
|
|
8
10
|
statisticalParityDifference,
|
|
9
11
|
} from "../src/fairness-metrics";
|
|
10
|
-
import type {
|
|
12
|
+
import type { SequenceLogProbProvider } from "../src/inference";
|
|
13
|
+
import type { ScoredLogProb } from "../src/inference/transformers-inference";
|
|
14
|
+
import type { BiasTestConfig, PredictionData } from "../src/types";
|
|
15
|
+
|
|
16
|
+
/** A provider whose scoring always throws — used to prove failures propagate. */
|
|
17
|
+
const alwaysFailingProvider: SequenceLogProbProvider = {
|
|
18
|
+
getScoredLogProb: async (): Promise<ScoredLogProb> => {
|
|
19
|
+
throw new Error("measurement unavailable");
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const alwaysSucceedingProvider: SequenceLogProbProvider = {
|
|
24
|
+
getScoredLogProb: async (text: string): Promise<ScoredLogProb> => ({
|
|
25
|
+
score: text.includes("stereo") ? -1 : -2,
|
|
26
|
+
scoredTokenIds: [],
|
|
27
|
+
span: { start: 0, end: 0 },
|
|
28
|
+
contextTokenHash: "test-hash",
|
|
29
|
+
provenance: {
|
|
30
|
+
modelId: "test-model",
|
|
31
|
+
logitsDtype: "float32",
|
|
32
|
+
backend: "test",
|
|
33
|
+
libraryVersion: "test",
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const config: BiasTestConfig = {
|
|
39
|
+
dataset: [
|
|
40
|
+
{ id: 1, sent_more: "stereo sentence", sent_less: "anti sentence", bias_type: "gender" },
|
|
41
|
+
],
|
|
42
|
+
model: "test-model",
|
|
43
|
+
engine: "browser",
|
|
44
|
+
};
|
|
11
45
|
|
|
12
46
|
describe("Fairness Metrics", () => {
|
|
13
47
|
describe("disparateImpact", () => {
|
|
@@ -184,4 +218,46 @@ describe("Fairness Metrics", () => {
|
|
|
184
218
|
expect(groups.size).toBeGreaterThan(1);
|
|
185
219
|
});
|
|
186
220
|
});
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* These entry points used to catch ANY calculateCrowsPairsBias failure —
|
|
224
|
+
* including the core's intentional fail-closed abort — and return a
|
|
225
|
+
* fabricated BiasTestResult (overallPassed: false, samplesAnalyzed: 0,
|
|
226
|
+
* method: "logprobs_fallback_latency") instead of propagating it. That is
|
|
227
|
+
* exactly the silent-fallback pattern this remediation removes: a failed
|
|
228
|
+
* measurement must never look like a plausible result object.
|
|
229
|
+
*/
|
|
230
|
+
describe("runCrowsPairsBiasAnalysis", () => {
|
|
231
|
+
it("returns a real result when measurement succeeds", async () => {
|
|
232
|
+
const result = await runCrowsPairsBiasAnalysis(config, alwaysSucceedingProvider);
|
|
233
|
+
expect(result.crowsPairsResult).toBeDefined();
|
|
234
|
+
expect(result.samplesAnalyzed).toBe(1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("propagates a measurement failure instead of fabricating a result", async () => {
|
|
238
|
+
await expect(runCrowsPairsBiasAnalysis(config, alwaysFailingProvider)).rejects.toThrow();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("runComprehensiveBiasAnalysis", () => {
|
|
243
|
+
it("returns a real result when measurement succeeds", async () => {
|
|
244
|
+
const result = await runComprehensiveBiasAnalysis(
|
|
245
|
+
[],
|
|
246
|
+
config,
|
|
247
|
+
alwaysSucceedingProvider,
|
|
248
|
+
"gender"
|
|
249
|
+
);
|
|
250
|
+
expect(result.crowsPairsResult).toBeDefined();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("propagates a measurement failure instead of fabricating or falling back to traditional-only metrics", async () => {
|
|
254
|
+
const predictionData: PredictionData[] = [
|
|
255
|
+
{ prediction: 1, protectedGroup: "A" },
|
|
256
|
+
{ prediction: 0, protectedGroup: "B" },
|
|
257
|
+
];
|
|
258
|
+
await expect(
|
|
259
|
+
runComprehensiveBiasAnalysis(predictionData, config, alwaysFailingProvider, "gender")
|
|
260
|
+
).rejects.toThrow();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
187
263
|
});
|