@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-validator.d.ts","sourceRoot":"","sources":["../../src/validation/schema-validator.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"schema-validator.d.ts","sourceRoot":"","sources":["../../src/validation/schema-validator.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,UAAU,GAClB,WAAW,GACX,UAAU,GACV,YAAY,GACZ,OAAO,GACP,WAAW,GACX,SAAS,GACT,SAAS,GACT,yBAAyB,CAAC;AAE9B,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAyCD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,gBAAgB,CAyEpF"}
|
|
@@ -22,6 +22,12 @@ const SCHEMA_MAP = {
|
|
|
22
22
|
"euconform.aibom.v1.1": { schemaFile: "aibom-v1.1.schema.json", type: "aibom.v1.1" },
|
|
23
23
|
"euconform.ci.v1": { schemaFile: "ci-v1.schema.json", type: "ci.v1" },
|
|
24
24
|
"euconform.bundle.v1": { schemaFile: "bundle-v1.schema.json", type: "bundle.v1" },
|
|
25
|
+
"euconform.bias.v1": { schemaFile: "bias-v1.schema.json", type: "bias.v1" },
|
|
26
|
+
"euconform.bias.v2": { schemaFile: "bias-v2.schema.json", type: "bias.v2" },
|
|
27
|
+
"euconform.external-observation.v1": {
|
|
28
|
+
schemaFile: "external-observation-v1.schema.json",
|
|
29
|
+
type: "external-observation.v1",
|
|
30
|
+
},
|
|
25
31
|
};
|
|
26
32
|
let ajvInstance = null;
|
|
27
33
|
function getAjv() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@euconform/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"license": "MIT OR EUPL-1.2",
|
|
5
5
|
"author": "Benedikt Hiepler",
|
|
6
6
|
"description": "Core engine for EU AI Act compliance, risk classification, and fairness metrics.",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"./types": "./src/types.ts",
|
|
31
31
|
"./scanner": "./src/scanner/index.ts",
|
|
32
32
|
"./evidence": "./src/evidence/index.ts",
|
|
33
|
+
"./evidence/browser": "./src/evidence/browser.ts",
|
|
33
34
|
"./datasets": "./src/datasets/crows-pairs.ts",
|
|
34
35
|
"./validation": "./src/validation/index.ts",
|
|
35
36
|
"./sbom": "./src/sbom/index.ts"
|
|
@@ -42,7 +43,8 @@
|
|
|
42
43
|
"@euconform/typescript-config": "0.0.0"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
|
-
"@
|
|
46
|
+
"@huggingface/transformers": "^4.2.0",
|
|
47
|
+
"onnxruntime-node": "1.24.3",
|
|
46
48
|
"ajv": "^8.18.0",
|
|
47
49
|
"ajv-formats": "^3.0.1",
|
|
48
50
|
"ignore": "^7.0.3"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Maintenance script: generates the checked-in golden fixture for the log-probability
|
|
4
|
+
* integration test (tests/inference/logprob-golden.integration.test.ts).
|
|
5
|
+
*
|
|
6
|
+
* This is the ONLY place that writes tests/inference/fixtures/logprob-golden.json.
|
|
7
|
+
* The integration test only READS it and never writes.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node --experimental-strip-types packages/core/scripts/seed-logprob-fixture.mjs
|
|
11
|
+
*
|
|
12
|
+
* Requires network access on first run (to download Xenova/gpt2 into the
|
|
13
|
+
* transformers.js cache) or a warm cache.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
17
|
+
import { createRequire, register } from "node:module";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const CORE_ROOT = join(SCRIPT_DIR, "..");
|
|
23
|
+
|
|
24
|
+
register(join(SCRIPT_DIR, "ts-extensionless-resolve-hook.mjs"), import.meta.url);
|
|
25
|
+
|
|
26
|
+
const { BrowserInference } = await import(join(CORE_ROOT, "src/inference/browser-inference.ts"));
|
|
27
|
+
|
|
28
|
+
const MODEL_ID = "Xenova/gpt2";
|
|
29
|
+
const HF_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the CURRENT commit SHA of the model repo on the Hugging Face Hub.
|
|
33
|
+
*
|
|
34
|
+
* "main" is a mutable branch pointer, not a pin: a future push to Xenova/gpt2 would
|
|
35
|
+
* silently move it and invalidate this fixture without anyone noticing. This is a
|
|
36
|
+
* tiny metadata fetch (not a model download), so we resolve it eagerly at seed time
|
|
37
|
+
* and pass the immutable SHA to `from_pretrained` as the `revision`. This script is
|
|
38
|
+
* online-by-nature (it downloads model weights), so a failure here aborts loudly
|
|
39
|
+
* rather than silently falling back to "main".
|
|
40
|
+
*
|
|
41
|
+
* Observed in this environment: a bare `fetch` sometimes returns a stale/cached
|
|
42
|
+
* schema-shaped stub instead of the real API response. Sending `Cache-Control:
|
|
43
|
+
* no-cache` reliably returns the real payload; we still validate the `sha` field's
|
|
44
|
+
* shape before trusting it, and retry a few times before giving up.
|
|
45
|
+
*/
|
|
46
|
+
async function resolveModelRevisionSha(modelId, { attempts = 3 } = {}) {
|
|
47
|
+
const url = `https://huggingface.co/api/models/${modelId}`;
|
|
48
|
+
let lastError;
|
|
49
|
+
|
|
50
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
headers: { "Cache-Control": "no-cache", Accept: "application/json" },
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
57
|
+
}
|
|
58
|
+
const body = await response.json();
|
|
59
|
+
const sha = body?.sha;
|
|
60
|
+
if (typeof sha !== "string" || !HF_SHA_PATTERN.test(sha)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`response did not contain a valid 40-char commit sha (got: ${JSON.stringify(sha)}); this looks like a malformed or stubbed API response, not the real Hub metadata`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return sha;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
lastError = err;
|
|
68
|
+
console.warn(
|
|
69
|
+
`[seed-logprob-fixture] revision resolution attempt ${attempt}/${attempts} failed: ${err.message}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Could not resolve an immutable commit sha for "${modelId}" from the Hugging Face Hub API ` +
|
|
76
|
+
`after ${attempts} attempts. Aborting: seeding against the mutable "main" ref would silently ` +
|
|
77
|
+
`invalidate this fixture on the next upstream push. Last error: ${lastError?.message}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 5 sentences per the binding spec:
|
|
82
|
+
// - 2 German sentences forming a gender minimal pair, taken from a clinical
|
|
83
|
+
// question catalogue; they differ in exactly one token so the fixture also
|
|
84
|
+
// covers the equal-token path
|
|
85
|
+
// - 1 additional German sentence, umlaut-heavy
|
|
86
|
+
// - 1 English sentence
|
|
87
|
+
// - 1 long German sentence
|
|
88
|
+
const SENTENCES = [
|
|
89
|
+
"Die Patientin übertreibt ihre Kopfschmerzen.",
|
|
90
|
+
"Der Patient übertreibt seine Kopfschmerzen.",
|
|
91
|
+
"Ärztinnen und Ärzte prüfen die Kopfschmerzen sorgfältig, bevor sie eine Überweisung veranlassen.",
|
|
92
|
+
"The patient reports a sudden and severe headache.",
|
|
93
|
+
"Nach ausführlicher Anamnese, wiederholter neurologischer Untersuchung und Rücksprache mit der Kollegin wurde entschieden, dass eine bildgebende Abklärung der Kopfschmerzen derzeit nicht dringlich erforderlich ist.",
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
function sha256HexOfIds(ids) {
|
|
97
|
+
return createHash("sha256").update(JSON.stringify(ids), "utf8").digest("hex");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function round6(n) {
|
|
101
|
+
return Math.round(n * 1e6) / 1e6;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The installed onnxruntime-node version. Not caught-and-"unknown": Messmotor-Sanierung
|
|
106
|
+
* Task 4 proved that an onnxruntime-node version bump alone (1.14.0 -> 1.24.3, byte-identical
|
|
107
|
+
* model weights, same dtype) shifts sequence log-probabilities at the 1e-6 relative level.
|
|
108
|
+
* This fixture exists to catch exactly that kind of silent drift, so it must record which
|
|
109
|
+
* onnxruntime-node version produced it — a script that can't resolve this must abort loudly,
|
|
110
|
+
* not silently write a fixture without it.
|
|
111
|
+
*/
|
|
112
|
+
function resolveOnnxRuntimeVersion() {
|
|
113
|
+
const require = createRequire(import.meta.url);
|
|
114
|
+
const pkg = require("onnxruntime-node/package.json");
|
|
115
|
+
if (!pkg.version) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"seed-logprob-fixture: could not resolve the installed onnxruntime-node version " +
|
|
118
|
+
"(missing 'version' field in onnxruntime-node/package.json). Aborting rather than " +
|
|
119
|
+
"writing a fixture missing this ergebnisrelevant field."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return pkg.version;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function main() {
|
|
126
|
+
const revision = await resolveModelRevisionSha(MODEL_ID);
|
|
127
|
+
console.log(`resolved revision (immutable commit sha): ${revision}`);
|
|
128
|
+
const onnxruntimeVersion = resolveOnnxRuntimeVersion();
|
|
129
|
+
console.log(`resolved onnxruntime-node version: ${onnxruntimeVersion}`);
|
|
130
|
+
|
|
131
|
+
// dtype: "fp32" -- full precision. Log-probability scoring for bias measurement
|
|
132
|
+
// must not carry quantization noise; the fixture is a numerical truth anchor.
|
|
133
|
+
const engine = new BrowserInference(MODEL_ID, null, { revision, dtype: "fp32" });
|
|
134
|
+
await engine.load();
|
|
135
|
+
|
|
136
|
+
const sentenceResults = [];
|
|
137
|
+
for (const text of SENTENCES) {
|
|
138
|
+
const result = await engine.getScoredLogProb(text);
|
|
139
|
+
sentenceResults.push({
|
|
140
|
+
text,
|
|
141
|
+
score: round6(result.score),
|
|
142
|
+
span: result.span,
|
|
143
|
+
scoredTokenIdsHash: sha256HexOfIds(result.scoredTokenIds),
|
|
144
|
+
scoredTokenCount: result.scoredTokenIds.length,
|
|
145
|
+
contextTokenHash: result.contextTokenHash,
|
|
146
|
+
provenance: result.provenance,
|
|
147
|
+
});
|
|
148
|
+
console.log(
|
|
149
|
+
`scored: "${text.slice(0, 40)}${text.length > 40 ? "..." : ""}" -> ${round6(result.score)}`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fixture = {
|
|
154
|
+
modelId: MODEL_ID,
|
|
155
|
+
revision,
|
|
156
|
+
dtype: sentenceResults[0]?.provenance.dtype ?? "unknown",
|
|
157
|
+
logitsDtype: sentenceResults[0]?.provenance.logitsDtype ?? "unknown",
|
|
158
|
+
backend: "node-onnx",
|
|
159
|
+
transformersVersion: sentenceResults[0]?.provenance.libraryVersion ?? "unknown",
|
|
160
|
+
onnxruntimeVersion,
|
|
161
|
+
nodeVersion: process.version,
|
|
162
|
+
// Relative, not absolute: the score is a SUM over tokens, so its magnitude scales with
|
|
163
|
+
// sentence length. An absolute tolerance tightens arbitrarily for short sentences and
|
|
164
|
+
// loosens for long ones. The observed onnxruntime-node 1.14.0 -> 1.24.3 drift (Messmotor-
|
|
165
|
+
// Sanierung Task 4, byte-identical weights, same dtype) was ~1.16e-6 relative (max 3.55e-6
|
|
166
|
+
// across these 5 sentences); 1e-5 leaves an order of magnitude of headroom without being
|
|
167
|
+
// permissive enough to hide a real regression.
|
|
168
|
+
relativeTolerance: 1e-5,
|
|
169
|
+
generatedAt: new Date().toISOString(),
|
|
170
|
+
sentences: sentenceResults.map(({ provenance, ...rest }) => rest),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const fixturePath = join(CORE_ROOT, "tests/inference/fixtures/logprob-golden.json");
|
|
174
|
+
await mkdir(dirname(fixturePath), { recursive: true });
|
|
175
|
+
await writeFile(fixturePath, `${JSON.stringify(fixture, null, 2)}\n`, "utf8");
|
|
176
|
+
console.log(`\nFixture written to ${fixturePath}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await main();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Node.js module-resolution hook: when a relative specifier without an
|
|
3
|
+
* extension fails to resolve (the normal case for TypeScript sources compiled with
|
|
4
|
+
* `moduleResolution: "bundler"`, which intentionally omit ".ts" from import specifiers),
|
|
5
|
+
* retry once with ".ts" appended.
|
|
6
|
+
*
|
|
7
|
+
* This exists ONLY so that maintenance scripts can run directly against the
|
|
8
|
+
* TypeScript sources with `node --experimental-strip-types`, without needing a
|
|
9
|
+
* bundler or ts-node. It changes no application behavior.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
15
|
+
try {
|
|
16
|
+
return await nextResolve(specifier, context);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
const isBareRelative = specifier.startsWith(".") && !/\.[a-zA-Z]+$/.test(specifier);
|
|
19
|
+
if (!isBareRelative) throw err;
|
|
20
|
+
|
|
21
|
+
const candidate = new URL(`${specifier}.ts`, context.parentURL);
|
|
22
|
+
if (!existsSync(fileURLToPath(candidate))) throw err;
|
|
23
|
+
|
|
24
|
+
return nextResolve(candidate.href, context);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { dirname, resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -62,3 +63,76 @@ export function loadCrowsPairsDataset(language: "en" | "de"): CrowsPairsEntry[]
|
|
|
62
63
|
bias_type: pair.biasType,
|
|
63
64
|
}));
|
|
64
65
|
}
|
|
66
|
+
|
|
67
|
+
export interface CustomDatasetMetadata {
|
|
68
|
+
name: string;
|
|
69
|
+
version: string;
|
|
70
|
+
sha256: string;
|
|
71
|
+
pairCount: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load a user-supplied question catalog.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately separate from loadCrowsPairsDataset: that one resolves a bundled
|
|
78
|
+
* asset by language code, this one takes a path the caller chose. The hash goes
|
|
79
|
+
* into the evidence artifact so "Fragekatalog + Version" is verifiable rather
|
|
80
|
+
* than merely stated.
|
|
81
|
+
*/
|
|
82
|
+
export function loadCustomDataset(filePath: string): {
|
|
83
|
+
pairs: CrowsPairsEntry[];
|
|
84
|
+
metadata: CustomDatasetMetadata;
|
|
85
|
+
} {
|
|
86
|
+
const resolved = resolve(filePath);
|
|
87
|
+
if (!existsSync(resolved)) {
|
|
88
|
+
throw new Error(`Fragekatalog nicht gefunden: ${resolved}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const raw = readFileSync(resolved, "utf-8");
|
|
92
|
+
let parsed: unknown;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(raw);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error(`Fragekatalog ist kein gültiges JSON: ${resolved}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const doc = parsed as { metadata?: Record<string, unknown>; pairs?: unknown[] };
|
|
100
|
+
if (!Array.isArray(doc.pairs) || doc.pairs.length === 0) {
|
|
101
|
+
throw new Error(`Fragekatalog enthält keine Paare: ${resolved}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const pairs: CrowsPairsEntry[] = doc.pairs.map((entry, index) => {
|
|
105
|
+
const p = entry as Record<string, unknown>;
|
|
106
|
+
const stereotype = typeof p.stereotype === "string" ? p.stereotype : undefined;
|
|
107
|
+
const antiStereotype = typeof p.antiStereotype === "string" ? p.antiStereotype : undefined;
|
|
108
|
+
if (!stereotype || !antiStereotype) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Fragekatalog: Paar ${p.id ?? index} braucht 'stereotype' und 'antiStereotype' (${resolved})`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
id: typeof p.id === "number" ? p.id : index + 1,
|
|
115
|
+
sent_more: stereotype,
|
|
116
|
+
sent_less: antiStereotype,
|
|
117
|
+
bias_type: typeof p.biasType === "string" ? p.biasType : "unspecified",
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const meta = doc.metadata ?? {};
|
|
122
|
+
return {
|
|
123
|
+
pairs,
|
|
124
|
+
metadata: {
|
|
125
|
+
name: typeof meta.name === "string" ? meta.name : "unbenannter Katalog",
|
|
126
|
+
version: typeof meta.version === "string" ? meta.version : "unversioniert",
|
|
127
|
+
sha256: createHash("sha256").update(raw).digest("hex"),
|
|
128
|
+
// Deliberately NOT the resolved absolute filesystem path: euconform.bias.v2's
|
|
129
|
+
// `catalog` schema (additionalProperties: false) has no `path` field on purpose — an
|
|
130
|
+
// absolute, machine-specific path baked into an evidence artifact would defeat the
|
|
131
|
+
// talk's "you can recompute this" promise the moment the repo is cloned to a
|
|
132
|
+
// different machine (Messmotor-Sanierung Task 6 finding: `validate` rejected a real
|
|
133
|
+
// clinical-catalog run over exactly this field). The catalog's identity is already
|
|
134
|
+
// fully portable via name + version + sha256.
|
|
135
|
+
pairCount: pairs.length,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown rendering for `compareBiasRuns` output.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the comparison logic so the numbers stay pure and testable;
|
|
5
|
+
* this module only formats. Mirrors the tone of the existing bias report: no
|
|
6
|
+
* severity labels, no pass/fail verdict — just the self-checkable numbers and
|
|
7
|
+
* the significance of the observed shift.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { BiasComparison, BiasRunSummary } from "./bias-comparison";
|
|
11
|
+
import type { WilsonInterval } from "./bias-statistics";
|
|
12
|
+
|
|
13
|
+
function percent(value: number): string {
|
|
14
|
+
return `${value.toFixed(1)}%`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function signed(value: number, digits: number): string {
|
|
18
|
+
return `${value >= 0 ? "+" : ""}${value.toFixed(digits)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function points(value: number): string {
|
|
22
|
+
return `${value >= 0 ? "+" : ""}${value.toFixed(1)} pp`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function wilson(interval: WilsonInterval | null): string {
|
|
26
|
+
if (!interval) return "n/a";
|
|
27
|
+
return `[${percent(interval.lower * 100)}, ${percent(interval.upper * 100)}]`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pValue(value: number): string {
|
|
31
|
+
return value < 0.0001 ? value.toExponential(2) : value.toFixed(4);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runHeading(label: string, run: BiasRunSummary): string {
|
|
35
|
+
return `**${label}:** ${run.model} — ${run.dataset} (${run.generatedAt})`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function summarySection(comparison: BiasComparison): string[] {
|
|
39
|
+
const { baseline, candidate, deltas } = comparison;
|
|
40
|
+
const rows = [
|
|
41
|
+
`| Model | ${baseline.model} | ${candidate.model} | — |`,
|
|
42
|
+
`| Mean bias score | ${baseline.meanBiasScore.toFixed(4)} | ${candidate.meanBiasScore.toFixed(4)} | ${signed(deltas.meanBiasScore, 4)} |`,
|
|
43
|
+
`| Preference quote (bias > 0) | ${percent(baseline.preferencePercent)} (95% Wilson ${wilson(baseline.preferenceWilson95)}) | ${percent(candidate.preferencePercent)} (95% Wilson ${wilson(candidate.preferenceWilson95)}) | ${points(deltas.preferencePercentPoints)} |`,
|
|
44
|
+
`| Analyzed pairs | ${baseline.analyzedCount} | ${candidate.analyzedCount} | ${candidate.analyzedCount - baseline.analyzedCount} |`,
|
|
45
|
+
`| Ties (bias = 0) | ${baseline.tiesCount} | ${candidate.tiesCount} | ${candidate.tiesCount - baseline.tiesCount} |`,
|
|
46
|
+
`| Equal-token subset | ${baseline.equalTokenPairsCount} pairs | ${candidate.equalTokenPairsCount} pairs | ${candidate.equalTokenPairsCount - baseline.equalTokenPairsCount} |`,
|
|
47
|
+
`| Run status | ${baseline.runStatus} | ${candidate.runStatus} | — |`,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
"## Run Summary",
|
|
52
|
+
"",
|
|
53
|
+
"| Metric | Baseline | Candidate | Delta |",
|
|
54
|
+
"|--------|----------|-----------|-------|",
|
|
55
|
+
...rows,
|
|
56
|
+
"",
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pairedSection(comparison: BiasComparison): string[] {
|
|
61
|
+
const { paired } = comparison;
|
|
62
|
+
const lines = [
|
|
63
|
+
"## Paired Comparison",
|
|
64
|
+
"",
|
|
65
|
+
`- Pairs measured in both runs: ${paired.pairsCompared} (paired by ${paired.keyStrategy === "pairId" ? "pair id" : "sentence text"})`,
|
|
66
|
+
`- Only in baseline: ${paired.onlyInBaseline}, only in candidate: ${paired.onlyInCandidate}`,
|
|
67
|
+
`- Preference flips: baseline only ${paired.baselinePreferredOnly}, candidate only ${paired.candidatePreferredOnly}`,
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
if (!paired.mcnemarExact) {
|
|
71
|
+
lines.push("- McNemar exact test: n/a (no discordant pairs)");
|
|
72
|
+
} else {
|
|
73
|
+
const { discordantPairs, pValue: p, alpha, significant } = paired.mcnemarExact;
|
|
74
|
+
lines.push(
|
|
75
|
+
`- McNemar exact test (two-sided, ${discordantPairs} discordant pairs): p = ${pValue(p)} (alpha ${alpha}) → ${
|
|
76
|
+
significant ? "significant shift" : "no significant shift"
|
|
77
|
+
}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return [...lines, ""];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function biasTypeSection(comparison: BiasComparison): string[] {
|
|
85
|
+
if (comparison.byBiasType.length === 0) return [];
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
"## Preference By Bias Type",
|
|
89
|
+
"",
|
|
90
|
+
"| Bias type | Baseline | Candidate | Delta | Pairs (baseline / candidate) |",
|
|
91
|
+
"|-----------|----------|-----------|-------|------------------------------|",
|
|
92
|
+
...comparison.byBiasType.map(
|
|
93
|
+
(row) =>
|
|
94
|
+
`| ${row.biasType} | ${percent(row.baselinePreferencePercent)} | ${percent(row.candidatePreferencePercent)} | ${points(row.deltaPoints)} | ${row.baselineAnalyzedCount} / ${row.candidateAnalyzedCount} |`
|
|
95
|
+
),
|
|
96
|
+
"",
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function changesSection(comparison: BiasComparison): string[] {
|
|
101
|
+
if (comparison.largestChanges.length === 0) {
|
|
102
|
+
return ["## Per-Pair Changes", "", "No pairs changed between the two runs.", ""];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
"## Largest Per-Pair Changes",
|
|
107
|
+
"",
|
|
108
|
+
"| Pair | Bias type | Baseline | Candidate | Delta |",
|
|
109
|
+
"|------|-----------|----------|-----------|-------|",
|
|
110
|
+
...comparison.largestChanges.map(
|
|
111
|
+
(change) =>
|
|
112
|
+
`| ${change.pairId ?? "—"} | ${change.biasType ?? "unspecified"} | ${change.baselineBiasScore.toFixed(4)} | ${change.candidateBiasScore.toFixed(4)} | ${signed(change.delta, 4)} |`
|
|
113
|
+
),
|
|
114
|
+
"",
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function comparabilitySection(comparison: BiasComparison): string[] {
|
|
119
|
+
const lines = [`**Comparable:** ${comparison.comparable ? "yes" : "no"}`, ""];
|
|
120
|
+
|
|
121
|
+
if (!comparison.comparable) {
|
|
122
|
+
lines.push(
|
|
123
|
+
"The two runs did not answer the same question with the same measurement setup. The deltas below are not a measurement of model behaviour and MUST NOT be read as one:",
|
|
124
|
+
"",
|
|
125
|
+
...comparison.comparabilityIssues.map((issue) => `- ${issue}`),
|
|
126
|
+
""
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (comparison.environmentDifferences.length > 0) {
|
|
131
|
+
lines.push(
|
|
132
|
+
"## Environment Differences",
|
|
133
|
+
"",
|
|
134
|
+
"Expected for model-to-model comparisons; listed so the difference stays visible:",
|
|
135
|
+
"",
|
|
136
|
+
...comparison.environmentDifferences.map((difference) => `- ${difference}`),
|
|
137
|
+
""
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return lines;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function formatBiasComparisonMarkdown(comparison: BiasComparison): string {
|
|
145
|
+
return [
|
|
146
|
+
"# EuConform Bias Comparison",
|
|
147
|
+
"",
|
|
148
|
+
runHeading("Baseline", comparison.baseline),
|
|
149
|
+
"",
|
|
150
|
+
runHeading("Candidate", comparison.candidate),
|
|
151
|
+
"",
|
|
152
|
+
...comparabilitySection(comparison),
|
|
153
|
+
...summarySection(comparison),
|
|
154
|
+
...pairedSection(comparison),
|
|
155
|
+
...biasTypeSection(comparison),
|
|
156
|
+
...changesSection(comparison),
|
|
157
|
+
"---",
|
|
158
|
+
"",
|
|
159
|
+
"*Generated by [EuConform CLI](https://github.com/Hiepler/EuConform). Comparison of reproducible CrowS-Pairs measurements; preference means the stereotype sentence scored higher than the anti-stereotype sentence.*",
|
|
160
|
+
"",
|
|
161
|
+
].join("\n");
|
|
162
|
+
}
|