@euconform/core 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +46 -0
  3. package/dist/datasets/crows-pairs.d.ts +18 -0
  4. package/dist/datasets/crows-pairs.d.ts.map +1 -1
  5. package/dist/datasets/crows-pairs.js +58 -0
  6. package/dist/evidence/bias-comparison-markdown.d.ts +11 -0
  7. package/dist/evidence/bias-comparison-markdown.d.ts.map +1 -0
  8. package/dist/evidence/bias-comparison-markdown.js +120 -0
  9. package/dist/evidence/bias-comparison.d.ts +133 -0
  10. package/dist/evidence/bias-comparison.d.ts.map +1 -0
  11. package/dist/evidence/bias-comparison.js +274 -0
  12. package/dist/evidence/bias-statistics.d.ts +72 -0
  13. package/dist/evidence/bias-statistics.d.ts.map +1 -0
  14. package/dist/evidence/bias-statistics.js +122 -0
  15. package/dist/evidence/bias-v2-semantics.d.ts +19 -0
  16. package/dist/evidence/bias-v2-semantics.d.ts.map +1 -0
  17. package/dist/evidence/bias-v2-semantics.js +133 -0
  18. package/dist/evidence/browser.d.ts +18 -0
  19. package/dist/evidence/browser.d.ts.map +1 -0
  20. package/dist/evidence/browser.js +14 -0
  21. package/dist/evidence/bundle.d.ts +1 -0
  22. package/dist/evidence/bundle.d.ts.map +1 -1
  23. package/dist/evidence/bundle.js +2 -0
  24. package/dist/evidence/index.d.ts +9 -2
  25. package/dist/evidence/index.d.ts.map +1 -1
  26. package/dist/evidence/index.js +5 -1
  27. package/dist/evidence/output.d.ts.map +1 -1
  28. package/dist/evidence/output.js +1 -0
  29. package/dist/evidence/types.d.ts +183 -3
  30. package/dist/evidence/types.d.ts.map +1 -1
  31. package/dist/evidence/validate-bias-v2.d.ts +31 -0
  32. package/dist/evidence/validate-bias-v2.d.ts.map +1 -0
  33. package/dist/evidence/validate-bias-v2.js +40 -0
  34. package/dist/evidence/validate.d.ts +29 -2
  35. package/dist/evidence/validate.d.ts.map +1 -1
  36. package/dist/evidence/validate.js +96 -11
  37. package/dist/fairness-metrics.d.ts +15 -11
  38. package/dist/fairness-metrics.d.ts.map +1 -1
  39. package/dist/fairness-metrics.js +59 -183
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/inference/browser-inference.d.ts +6 -57
  44. package/dist/inference/browser-inference.d.ts.map +1 -1
  45. package/dist/inference/browser-inference.js +6 -219
  46. package/dist/inference/capability-detection.d.ts +9 -2
  47. package/dist/inference/capability-detection.d.ts.map +1 -1
  48. package/dist/inference/capability-detection.js +18 -16
  49. package/dist/inference/index.d.ts +15 -2
  50. package/dist/inference/index.d.ts.map +1 -1
  51. package/dist/inference/index.js +12 -3
  52. package/dist/inference/inference-factory.d.ts +6 -3
  53. package/dist/inference/inference-factory.d.ts.map +1 -1
  54. package/dist/inference/inference-factory.js +7 -26
  55. package/dist/inference/ollama-client.d.ts +29 -0
  56. package/dist/inference/ollama-client.d.ts.map +1 -1
  57. package/dist/inference/ollama-client.js +29 -0
  58. package/dist/inference/scored-log-prob-types.d.ts +64 -0
  59. package/dist/inference/scored-log-prob-types.d.ts.map +1 -0
  60. package/dist/inference/scored-log-prob-types.js +12 -0
  61. package/dist/inference/sequence-logprob.d.ts +34 -0
  62. package/dist/inference/sequence-logprob.d.ts.map +1 -0
  63. package/dist/inference/sequence-logprob.js +87 -0
  64. package/dist/inference/template-scoring.d.ts +124 -0
  65. package/dist/inference/template-scoring.d.ts.map +1 -0
  66. package/dist/inference/template-scoring.js +164 -0
  67. package/dist/inference/transformers-inference.d.ts +124 -0
  68. package/dist/inference/transformers-inference.d.ts.map +1 -0
  69. package/dist/inference/transformers-inference.js +392 -0
  70. package/dist/legal-checks/bias-metrics.d.ts +5 -7
  71. package/dist/legal-checks/bias-metrics.d.ts.map +1 -1
  72. package/dist/legal-checks/bias-metrics.js +8 -101
  73. package/dist/metrics/crows-pairs-bias.d.ts +17 -9
  74. package/dist/metrics/crows-pairs-bias.d.ts.map +1 -1
  75. package/dist/metrics/crows-pairs-bias.js +147 -59
  76. package/dist/metrics/stereotype-bias.d.ts +13 -5
  77. package/dist/metrics/stereotype-bias.d.ts.map +1 -1
  78. package/dist/metrics/stereotype-bias.js +16 -99
  79. package/dist/sbom/component-mapper.d.ts +26 -0
  80. package/dist/sbom/component-mapper.d.ts.map +1 -0
  81. package/dist/sbom/component-mapper.js +83 -0
  82. package/dist/sbom/cyclonedx-import.d.ts +34 -0
  83. package/dist/sbom/cyclonedx-import.d.ts.map +1 -0
  84. package/dist/sbom/cyclonedx-import.js +131 -0
  85. package/dist/sbom/index.d.ts +6 -0
  86. package/dist/sbom/index.d.ts.map +1 -0
  87. package/dist/sbom/index.js +4 -0
  88. package/dist/sbom/known-packages.d.ts +13 -0
  89. package/dist/sbom/known-packages.d.ts.map +1 -0
  90. package/dist/sbom/known-packages.js +129 -0
  91. package/dist/sbom/purl.d.ts +14 -0
  92. package/dist/sbom/purl.d.ts.map +1 -0
  93. package/dist/sbom/purl.js +56 -0
  94. package/dist/sbom/types.d.ts +39 -0
  95. package/dist/sbom/types.d.ts.map +1 -0
  96. package/dist/sbom/types.js +5 -0
  97. package/dist/scanner/detectors/ai-framework-detector.js +1 -1
  98. package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -1
  99. package/dist/scanner/detectors/local-inference-detector.js +1 -1
  100. package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -1
  101. package/dist/scanner/detectors/package-json-detector.js +1 -0
  102. package/dist/types.d.ts +103 -2
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/validation/index.d.ts +2 -0
  105. package/dist/validation/index.d.ts.map +1 -0
  106. package/dist/validation/index.js +1 -0
  107. package/dist/validation/schema-validator.d.ts +18 -0
  108. package/dist/validation/schema-validator.d.ts.map +1 -0
  109. package/dist/validation/schema-validator.js +105 -0
  110. package/package.json +9 -3
  111. package/scripts/seed-logprob-fixture.mjs +179 -0
  112. package/scripts/ts-extensionless-resolve-hook.mjs +26 -0
  113. package/src/datasets/crows-pairs.ts +74 -0
  114. package/src/evidence/bias-comparison-markdown.ts +162 -0
  115. package/src/evidence/bias-comparison.ts +432 -0
  116. package/src/evidence/bias-statistics.ts +180 -0
  117. package/src/evidence/bias-v2-semantics.ts +214 -0
  118. package/src/evidence/browser.ts +35 -0
  119. package/src/evidence/bundle.ts +2 -0
  120. package/src/evidence/index.ts +31 -0
  121. package/src/evidence/output.ts +1 -0
  122. package/src/evidence/types.ts +213 -3
  123. package/src/evidence/validate-bias-v2.ts +47 -0
  124. package/src/evidence/validate.ts +151 -17
  125. package/src/fairness-metrics.ts +79 -222
  126. package/src/index.ts +7 -0
  127. package/src/inference/browser-inference.ts +14 -256
  128. package/src/inference/capability-detection.ts +32 -18
  129. package/src/inference/index.ts +33 -5
  130. package/src/inference/inference-factory.ts +9 -30
  131. package/src/inference/ollama-client.ts +29 -0
  132. package/src/inference/scored-log-prob-types.ts +66 -0
  133. package/src/inference/sequence-logprob.ts +136 -0
  134. package/src/inference/template-scoring.ts +248 -0
  135. package/src/inference/transformers-inference.ts +494 -0
  136. package/src/legal-checks/bias-metrics.ts +20 -118
  137. package/src/metrics/crows-pairs-bias.ts +180 -83
  138. package/src/metrics/stereotype-bias.ts +27 -119
  139. package/src/sbom/component-mapper.ts +107 -0
  140. package/src/sbom/cyclonedx-import.ts +198 -0
  141. package/src/sbom/index.ts +12 -0
  142. package/src/sbom/known-packages.ts +142 -0
  143. package/src/sbom/purl.ts +66 -0
  144. package/src/sbom/types.ts +34 -0
  145. package/src/scanner/detectors/ai-framework-detector.ts +1 -1
  146. package/src/scanner/detectors/local-inference-detector.ts +2 -1
  147. package/src/scanner/detectors/package-json-detector.ts +1 -0
  148. package/src/types.ts +108 -2
  149. package/src/validation/index.ts +7 -0
  150. package/src/validation/schema-validator.ts +147 -0
  151. package/tests/annex-iv-report.test.ts +1 -1
  152. package/tests/capability-detection.test.ts +57 -0
  153. package/tests/crows-pairs-bias.test.ts +286 -186
  154. package/tests/custom-dataset.test.ts +80 -0
  155. package/tests/evidence/bias-artifact-v2.test.ts +135 -0
  156. package/tests/evidence/bias-artifact.test.ts +78 -0
  157. package/tests/evidence/bias-comparison.test.ts +397 -0
  158. package/tests/evidence/bias-statistics.test.ts +191 -0
  159. package/tests/evidence/external-observation.test.ts +167 -0
  160. package/tests/evidence/spec-schemas.test.ts +35 -2
  161. package/tests/evidence/validate-bias-v2.test.ts +180 -0
  162. package/tests/fairness-metrics.test.ts +77 -1
  163. package/tests/fixtures/cyclonedx-minimal.json +13 -0
  164. package/tests/fixtures/cyclonedx-mixed.json +17 -0
  165. package/tests/fixtures/cyclonedx-ml-bom.json +25 -0
  166. package/tests/inference/browser-inference-scoring.test.ts +159 -0
  167. package/tests/inference/fixtures/logprob-golden.json +69 -0
  168. package/tests/inference/gemma-template.integration.test.ts +81 -0
  169. package/tests/inference/logprob-golden.integration.test.ts +128 -0
  170. package/tests/inference/synthetic-logits.test.ts +188 -0
  171. package/tests/inference/template-scoring.test.ts +249 -0
  172. package/tests/inference/transformers-inference-template-mode.test.ts +214 -0
  173. package/tests/integration/bias-calculation.integration.test.ts +119 -599
  174. package/tests/sbom/component-mapper.test.ts +258 -0
  175. package/tests/sbom/cyclonedx-import.test.ts +329 -0
  176. package/tests/sbom/purl.test.ts +102 -0
  177. package/tests/stilled-ollama-paths.test.ts +76 -0
  178. package/tests/validation/schema-validator.test.ts +219 -0
  179. package/src/inference/transformers.d.ts +0 -62
@@ -0,0 +1,494 @@
1
+ /**
2
+ * Browser-based inference using transformers.js
3
+ * For small models (≤3B params) that can run via WebGPU
4
+ *
5
+ * STRICT MODE: Real inference only. No demo data.
6
+ */
7
+
8
+ import type {
9
+ DataType,
10
+ PreTrainedModel,
11
+ PreTrainedTokenizer,
12
+ Tensor,
13
+ } from "@huggingface/transformers";
14
+ import type { ScoredLogProb } from "./scored-log-prob-types";
15
+ import { type LogitsTensorLike, type Span, sumSequenceLogProb } from "./sequence-logprob";
16
+ import {
17
+ type MinimalChatTemplateTokenizer,
18
+ type MinimalModelConfig,
19
+ type TemplateModel,
20
+ type TemplateTokenizer,
21
+ hasChatTemplate,
22
+ isConditionalGenerationConfig,
23
+ scoreTemplateContinuation,
24
+ } from "./template-scoring";
25
+
26
+ /** Numeric precision for model weights — re-exported so callers don't need to import from
27
+ * @huggingface/transformers directly. See DATA_TYPES in that package for the full list
28
+ * (includes quantization schemes like "q4", "bnb4"); the scoring path must use "fp32". */
29
+ export type TransformersDType = DataType;
30
+
31
+ // `ScoredLogProb`/`ScoredLogProbProvenance` moved to `./scored-log-prob-types` in Task 5, to
32
+ // let `template-scoring.ts` depend on them without a circular import; re-exported here so
33
+ // existing `import { type ScoredLogProb } from "./transformers-inference"` sites keep working.
34
+ export type { ScoredLogProb, ScoredLogProbProvenance } from "./scored-log-prob-types";
35
+
36
+ async function sha256Hex(input: string): Promise<string> {
37
+ const bytes = new TextEncoder().encode(input);
38
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
39
+ return Array.from(new Uint8Array(digest))
40
+ .map((b) => b.toString(16).padStart(2, "0"))
41
+ .join("");
42
+ }
43
+
44
+ async function resolveTransformersVersion(): Promise<string> {
45
+ try {
46
+ // v4's package.json declares a strict `exports` map with no "./package.json" subpath
47
+ // (unlike v2's), so `require("@huggingface/transformers/package.json")` throws
48
+ // ERR_PACKAGE_PATH_NOT_EXPORTED — surfaced while fixing Messmotor-Sanierung Task 4's
49
+ // onnxruntimeVersion provenance gap, where this exact function was silently returning
50
+ // "unknown" on every real run. `env.version` is v4's own documented public API for this
51
+ // and isn't subject to the exports map at all.
52
+ const { env } = await import("@huggingface/transformers");
53
+ return env.version ?? "unknown";
54
+ } catch {
55
+ return "unknown";
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Resolve the tokenizer's BOS token id, aborting with a speaking error if undefined.
61
+ *
62
+ * @huggingface/transformers v4 exposes `bos_token` / `bos_token_id` directly as typed
63
+ * properties on `PreTrainedTokenizer` — the v2 lookup dance (`getToken("bos_token")` then
64
+ * `tokenizer.model.tokens_to_ids.get(...)`) is gone; deleting transformers.d.ts surfaced
65
+ * this (Messmotor-Sanierung Task 4, trap 3).
66
+ */
67
+ function resolveBosTokenId(tokenizer: PreTrainedTokenizer, modelId: string): number {
68
+ const bosToken = tokenizer.bos_token;
69
+ if (!bosToken) {
70
+ throw new Error(
71
+ `getScoredLogProb: tokenizer for model "${modelId}" has no bos_token defined in its config; cannot construct a BOS-prefixed input for log-probability scoring.`
72
+ );
73
+ }
74
+ const bosTokenId = tokenizer.bos_token_id;
75
+ if (bosTokenId === undefined || bosTokenId === null) {
76
+ throw new Error(
77
+ `getScoredLogProb: tokenizer for model "${modelId}" could not resolve bos_token "${bosToken}" to a token id.`
78
+ );
79
+ }
80
+ return bosTokenId;
81
+ }
82
+
83
+ export interface BrowserModel {
84
+ id: string;
85
+ name: string;
86
+ size: string;
87
+ supported: boolean;
88
+ }
89
+
90
+ // Supported models for browser inference
91
+ export const BROWSER_MODELS: BrowserModel[] = [
92
+ {
93
+ id: "Xenova/phi-3-mini-4k-instruct",
94
+ name: "Phi-3 Mini",
95
+ size: "3.8B",
96
+ supported: true,
97
+ },
98
+ {
99
+ id: "Xenova/Phi-3.5-mini-instruct",
100
+ name: "Phi-3.5 Mini",
101
+ size: "3.8B",
102
+ supported: true,
103
+ },
104
+ {
105
+ id: "Xenova/gpt2",
106
+ name: "GPT-2",
107
+ size: "124M",
108
+ supported: true,
109
+ },
110
+ {
111
+ id: "Xenova/distilgpt2",
112
+ name: "DistilGPT-2",
113
+ size: "82M",
114
+ supported: true,
115
+ },
116
+ ];
117
+
118
+ /**
119
+ * Check if WebGPU is available in the browser
120
+ */
121
+ export async function checkWebGPU(): Promise<boolean> {
122
+ if (typeof navigator === "undefined") return false;
123
+ if (typeof window === "undefined") return false;
124
+
125
+ try {
126
+ // Check for WebGPU support
127
+ const gpu = (navigator as Navigator & { gpu?: unknown }).gpu;
128
+ if (!gpu) return false;
129
+ // Try to get adapter
130
+ const adapter = await (gpu as { requestAdapter: () => Promise<unknown> }).requestAdapter();
131
+ return adapter !== null;
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Check if transformers.js is available
139
+ */
140
+ export async function checkTransformersAvailable(): Promise<boolean> {
141
+ try {
142
+ // Try dynamic import - this will fail if package not installed
143
+ await import("@huggingface/transformers");
144
+ return true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Browser/Node inference engine using transformers.js — the single measurement
152
+ * engine for bias log-probabilities (see Messmotor-Sanierung: Ollama's logprobs
153
+ * only cover generated tokens, never prompt/sentence log-probability).
154
+ * STRICT MODE: Throws error if package not installed.
155
+ *
156
+ * Named `TransformersInference` (formerly `BrowserInference`); the old name is
157
+ * re-exported from `browser-inference.ts` for backward compatibility.
158
+ */
159
+ export class TransformersInference {
160
+ public readonly modelId: string;
161
+ private model: PreTrainedModel | null = null;
162
+ private tokenizer: PreTrainedTokenizer | null = null;
163
+ private loadingPromise: Promise<void> | null = null;
164
+ private loadError: Error | null = null;
165
+ private statusCallback: ((status: string) => void) | null = null;
166
+ private readonly revision: string;
167
+ /**
168
+ * Public (not private) so callers building their own provenance/artifact records (e.g.
169
+ * the CLI's `run-bias-test.ts`) can read the CONFIGURED weight precision actually used to
170
+ * measure, instead of duplicating it as a separate hardcoded literal that could silently
171
+ * drift out of sync with the constructor argument (Messmotor-Sanierung Task 5 review
172
+ * finding).
173
+ */
174
+ public readonly dtype: TransformersDType;
175
+ /**
176
+ * Selected from the loaded TOKENIZER's chat template (`hasChatTemplate`), never from the
177
+ * model's architecture name and never from `modelId` — see `hasChatTemplate`
178
+ * (Messmotor-Sanierung Task 6c; supersedes the architecture-keyed detection from Task 5,
179
+ * which missed instruction-tuned `*ForCausalLM` models). `true` routes `getScoredLogProb`
180
+ * through the template-mode scorer instead of the Task 1 base (bare-sentence) mode.
181
+ *
182
+ * Independent of `loadTemplateMode`/`loadBaseMode` below, which pick the LOADER
183
+ * (AutoProcessor+Gemma4ForConditionalGeneration vs. AutoTokenizer+AutoModelForCausalLM)
184
+ * and stay keyed on architecture — a CausalLM-architecture instruction model (e.g.
185
+ * Llama-3.2-1B-Instruct) uses the base-mode loader but the template-mode scorer.
186
+ */
187
+ private isTemplateMode = false;
188
+
189
+ constructor(
190
+ modelId = "Xenova/distilgpt2",
191
+ statusCallback: ((status: string) => void) | null = null,
192
+ options: { revision?: string; dtype?: TransformersDType } = {}
193
+ ) {
194
+ this.modelId = modelId;
195
+ this.statusCallback = statusCallback;
196
+ this.revision = options.revision ?? "main";
197
+ // Default "q8" preserves the prior effective behavior for browser generation
198
+ // (smaller/faster; the v2 default was `quantized: true`, which loaded 8-bit
199
+ // weights). Log-probability scoring for bias measurement must pass `dtype: "fp32"`
200
+ // explicitly (full precision) to avoid quantization noise in the measurement
201
+ // itself — see run-bias-test.ts and the golden integration test.
202
+ this.dtype = options.dtype ?? "q8";
203
+ }
204
+
205
+ /**
206
+ * Check if browser inference is available
207
+ */
208
+ async isAvailable(): Promise<boolean> {
209
+ const hasTransformers = await checkTransformersAvailable();
210
+ return hasTransformers;
211
+ }
212
+
213
+ /**
214
+ * Load the model (lazy loading)
215
+ * Handles concurrency with loadingPromise
216
+ */
217
+ async load(): Promise<void> {
218
+ if (this.model && this.tokenizer) return;
219
+ if (this.loadingPromise) return this.loadingPromise;
220
+ if (this.loadError) throw this.loadError;
221
+
222
+ this.loadingPromise = this.performLoad().finally(() => {
223
+ this.loadingPromise = null;
224
+ });
225
+
226
+ return this.loadingPromise;
227
+ }
228
+
229
+ private notifyStatus(status: string): void {
230
+ if (this.statusCallback) this.statusCallback(status);
231
+ }
232
+
233
+ private async resolveDevice(): Promise<"webgpu" | "wasm" | "cpu"> {
234
+ const hasWebGPU = await checkWebGPU();
235
+ // v4 device names: Node's CPU backend is "cpu"; the browser's non-GPU backend is
236
+ // "wasm" (there is no "cpu" device in a browser). checkWebGPU() always resolves to
237
+ // false under Node, so this only matters for the (typeof window !== "undefined")
238
+ // branch — Messmotor-Sanierung Task 4, trap 2.
239
+ const isBrowserEnv = typeof window !== "undefined";
240
+ return hasWebGPU ? "webgpu" : isBrowserEnv ? "wasm" : "cpu";
241
+ }
242
+
243
+ /**
244
+ * Conditional-generation/multimodal architecture (e.g. Gemma-4): AutoProcessor +
245
+ * Gemma4ForConditionalGeneration, per Task 5. `Gemma4ForConditionalGeneration` is
246
+ * exported directly by @huggingface/transformers@4.2.0 under that exact name (the ONNX
247
+ * repo's own example script uses the same pair) — no fallback to AutoModelForCausalLM is
248
+ * used or needed for this architecture.
249
+ */
250
+ private async loadTemplateMode(
251
+ transformers: Pick<
252
+ Awaited<typeof import("@huggingface/transformers")>,
253
+ "AutoTokenizer" | "AutoProcessor" | "Gemma4ForConditionalGeneration"
254
+ >,
255
+ device: "webgpu" | "wasm" | "cpu"
256
+ ): Promise<void> {
257
+ const { AutoTokenizer, AutoProcessor, Gemma4ForConditionalGeneration } = transformers;
258
+
259
+ this.notifyStatus("loading-processor");
260
+ const processor = await AutoProcessor.from_pretrained(this.modelId, {
261
+ revision: this.revision,
262
+ });
263
+ this.tokenizer =
264
+ processor.tokenizer ??
265
+ (await AutoTokenizer.from_pretrained(this.modelId, { revision: this.revision }));
266
+
267
+ this.notifyStatus("loading-model");
268
+ this.model = await Gemma4ForConditionalGeneration.from_pretrained(this.modelId, {
269
+ device,
270
+ dtype: this.dtype,
271
+ revision: this.revision,
272
+ });
273
+ }
274
+
275
+ /** Base mode (Task 1): classic causal LM, unchanged behavior. */
276
+ private async loadBaseMode(
277
+ transformers: Pick<
278
+ Awaited<typeof import("@huggingface/transformers")>,
279
+ "AutoTokenizer" | "AutoModelForCausalLM"
280
+ >,
281
+ device: "webgpu" | "wasm" | "cpu"
282
+ ): Promise<void> {
283
+ const { AutoTokenizer, AutoModelForCausalLM } = transformers;
284
+
285
+ this.notifyStatus("loading-tokenizer");
286
+ this.tokenizer = await AutoTokenizer.from_pretrained(this.modelId, {
287
+ revision: this.revision,
288
+ });
289
+
290
+ this.notifyStatus("loading-model");
291
+ this.model = await AutoModelForCausalLM.from_pretrained(this.modelId, {
292
+ device,
293
+ dtype: this.dtype,
294
+ revision: this.revision,
295
+ });
296
+ }
297
+
298
+ private async performLoad() {
299
+ this.notifyStatus("loading");
300
+
301
+ try {
302
+ const transformers = await import("@huggingface/transformers");
303
+
304
+ // LOADER selection is from the loaded CONFIG (`architectures`), never from `modelId`
305
+ // — see `isConditionalGenerationConfig` (Messmotor-Sanierung Task 5, ambiguity
306
+ // resolution 1). This is a loader decision only (which `from_pretrained` class pair
307
+ // to call); the SCORING MODE is decided separately below, from the tokenizer's chat
308
+ // template (Task 6c) — the two must not be conflated. This one extra
309
+ // `AutoConfig.from_pretrained` call runs for every model (base mode included); it
310
+ // hits the same on-disk cache as the tokenizer/model downloads, so the added cost is
311
+ // negligible next to those.
312
+ this.notifyStatus("loading-config");
313
+ const config = await transformers.AutoConfig.from_pretrained(this.modelId, {
314
+ revision: this.revision,
315
+ });
316
+ const useConditionalGenerationLoader = isConditionalGenerationConfig(
317
+ config as unknown as MinimalModelConfig
318
+ );
319
+
320
+ const device = await this.resolveDevice();
321
+
322
+ if (useConditionalGenerationLoader) {
323
+ await this.loadTemplateMode(transformers, device);
324
+ } else {
325
+ await this.loadBaseMode(transformers, device);
326
+ }
327
+
328
+ if (!this.tokenizer) {
329
+ throw new Error(
330
+ `performLoad: tokenizer missing after load for model "${this.modelId}" — cannot determine scoring mode.`
331
+ );
332
+ }
333
+ // SCORING MODE selection (Messmotor-Sanierung Task 6c): keyed on the loaded
334
+ // tokenizer's chat template, independent of the loader chosen above — see
335
+ // `hasChatTemplate`.
336
+ this.isTemplateMode = hasChatTemplate(
337
+ this.tokenizer as unknown as MinimalChatTemplateTokenizer
338
+ );
339
+
340
+ this.notifyStatus("ready");
341
+ } catch (err) {
342
+ this.loadError = err instanceof Error ? err : new Error(String(err));
343
+ this.notifyStatus("error");
344
+ throw this.loadError;
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Generate text.
350
+ *
351
+ * v4 note: previously this called a `pipeline("text-generation", ...)` built from the
352
+ * already-loaded model/tokenizer, which transformers.js re-resolves/re-loads internally
353
+ * — effectively a second model load that `getScoredLogProb`/`getLogProb` never used
354
+ * (Messmotor-Sanierung Task 4, trap 4). Calling `model.generate()` directly avoids that.
355
+ */
356
+ async generate(prompt: string, maxTokens = 50): Promise<string> {
357
+ await this.load();
358
+
359
+ if (!this.tokenizer || !this.model) {
360
+ throw new Error("Model not loaded");
361
+ }
362
+
363
+ const tokenizer = this.tokenizer;
364
+ const model = this.model;
365
+
366
+ const { input_ids, attention_mask } = tokenizer(prompt, { return_tensor: true });
367
+ const outputs = await model.generate({
368
+ inputs: input_ids,
369
+ attention_mask,
370
+ max_new_tokens: maxTokens,
371
+ temperature: 0.7,
372
+ do_sample: true,
373
+ });
374
+
375
+ // model.generate()'s declared return type is `ModelOutput | Tensor`, but for causal LM
376
+ // decoding (no encoder-decoder, no streamer) it is always a plain Tensor of token ids —
377
+ // narrow via `unknown` rather than `any` since the union has no discriminant we can check.
378
+ const decoded = tokenizer.batch_decode(outputs as unknown as Tensor, {
379
+ skip_special_tokens: true,
380
+ });
381
+ return decoded[0] ?? "";
382
+ }
383
+
384
+ /**
385
+ * Score a sentence's log-probability with full provenance.
386
+ * This is the "Gold Standard" for bias testing.
387
+ *
388
+ * BOS fix: GPT-2-family tokenizers do not insert a BOS token on their own, so the
389
+ * sentence is tokenized WITHOUT special tokens and a BOS token is explicitly
390
+ * prepended before feeding the model (input_ids and attention_mask are rebuilt
391
+ * together). This ensures the model sees P(first_token | BOS) instead of silently
392
+ * skipping the first sentence token. BOS itself is context and is never scored;
393
+ * every sentence token is scored via the half-open span [1, n+1).
394
+ */
395
+ async getScoredLogProb(text: string): Promise<ScoredLogProb> {
396
+ await this.load();
397
+
398
+ if (!this.tokenizer || !this.model) {
399
+ throw new Error("Model not loaded");
400
+ }
401
+
402
+ if (this.isTemplateMode) {
403
+ const libraryVersion = await resolveTransformersVersion();
404
+ return scoreTemplateContinuation(
405
+ {
406
+ tokenizer: this.tokenizer as unknown as TemplateTokenizer,
407
+ model: this.model as unknown as TemplateModel,
408
+ modelId: this.modelId,
409
+ libraryVersion,
410
+ dtype: this.dtype,
411
+ },
412
+ text
413
+ );
414
+ }
415
+
416
+ const tokenizer = this.tokenizer;
417
+ const model = this.model;
418
+
419
+ const bosTokenId = resolveBosTokenId(tokenizer, this.modelId);
420
+
421
+ // v4 renamed the tokenizer call's tensor-output option from `return_tensors: "pt"`
422
+ // (a v2 backend-name string) to `return_tensor: true` (a plain boolean) — surfaced by
423
+ // deleting transformers.d.ts; `return_tensors: "pt"` is not a recognized v4 option and
424
+ // would silently fail to type-check (Messmotor-Sanierung Task 4).
425
+ const encoded = tokenizer(text, { return_tensor: true, add_special_tokens: false });
426
+ const inputIdsTensor = encoded.input_ids;
427
+ const [batchSize] = inputIdsTensor.dims;
428
+ if (batchSize !== 1) {
429
+ throw new Error(`getScoredLogProb: batch_size must be 1, got ${batchSize}`);
430
+ }
431
+
432
+ const sentenceTokenIds: number[] = Array.from(
433
+ inputIdsTensor.data as ArrayLike<bigint | number>,
434
+ (v) => Number(v)
435
+ );
436
+
437
+ // Guard against double-BOS if a future tokenizer config already inserts it.
438
+ const finalTokenIds =
439
+ sentenceTokenIds[0] === bosTokenId ? sentenceTokenIds : [bosTokenId, ...sentenceTokenIds];
440
+ const scoredTokenIds = finalTokenIds.slice(1);
441
+
442
+ const { Tensor } = await import("@huggingface/transformers");
443
+ const newLen = finalTokenIds.length;
444
+ const newInputIds = new Tensor("int64", BigInt64Array.from(finalTokenIds.map(BigInt)), [
445
+ 1,
446
+ newLen,
447
+ ]);
448
+ const newAttentionMask = new Tensor("int64", BigInt64Array.from(new Array(newLen).fill(1n)), [
449
+ 1,
450
+ newLen,
451
+ ]);
452
+
453
+ const outputs = await model({ input_ids: newInputIds, attention_mask: newAttentionMask });
454
+ const logits: LogitsTensorLike = outputs.logits;
455
+
456
+ const span: Span = { start: 1, end: newLen };
457
+ const score = sumSequenceLogProb(logits, finalTokenIds, span);
458
+
459
+ const contextTokenHash = await sha256Hex(JSON.stringify([bosTokenId]));
460
+ const libraryVersion = await resolveTransformersVersion();
461
+
462
+ return {
463
+ score,
464
+ scoredTokenIds,
465
+ span,
466
+ contextTokenHash,
467
+ provenance: {
468
+ modelId: this.modelId,
469
+ dtype: this.dtype,
470
+ logitsDtype: String((logits as unknown as { type?: string }).type ?? "unknown"),
471
+ backend: "node-onnx",
472
+ libraryVersion,
473
+ templateStrategy: "none",
474
+ },
475
+ method: "sequence_logprob_sum",
476
+ };
477
+ }
478
+
479
+ /**
480
+ * Calculate Log-Probability for a given text (score only).
481
+ * Thin wrapper around `getScoredLogProb` for callers that only need the score.
482
+ */
483
+ async getLogProb(text: string): Promise<number> {
484
+ const { score } = await this.getScoredLogProb(text);
485
+ return score;
486
+ }
487
+
488
+ /**
489
+ * Get list of supported browser models
490
+ */
491
+ static getSupportedModels(): BrowserModel[] {
492
+ return BROWSER_MODELS.filter((m) => m.supported);
493
+ }
494
+ }
@@ -11,8 +11,7 @@
11
11
 
12
12
  import { disparateImpact, equalizedOdds, statisticalParityDifference } from "../fairness-metrics";
13
13
  import type { InferenceClient } from "../inference";
14
- import type { BiasType, PairTestResult, StereotypeBiasResult, StereotypePair } from "../metrics";
15
- import { runStereotypeBiasTest } from "../metrics";
14
+ import type { BiasType, PairTestResult, StereotypePair } from "../metrics";
16
15
  import type { BiasTestResult, FairnessMetric, PredictionData } from "../types";
17
16
 
18
17
  export type SensitiveAttribute =
@@ -195,32 +194,6 @@ export function runArticle10NumericBiasSuite(params: {
195
194
  // Text / Generative model metrics (LLM)
196
195
  // ============================================================================
197
196
 
198
- function mulberry32(seed: number) {
199
- let state = seed >>> 0;
200
- return () => {
201
- state = (state + 0x6d2b79f5) >>> 0;
202
- let t = state;
203
- t = Math.imul(t ^ (t >>> 15), t | 1);
204
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
205
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
206
- };
207
- }
208
-
209
- function seededSample<T>(arr: T[], n: number, seed: number): T[] {
210
- const rng = mulberry32(seed);
211
- const copy = arr.slice();
212
- // Fisher-Yates
213
- for (let i = copy.length - 1; i > 0; i--) {
214
- const j = Math.floor(rng() * (i + 1));
215
- const a = copy[i];
216
- const b = copy[j];
217
- if (a === undefined || b === undefined) continue;
218
- copy[i] = b;
219
- copy[j] = a;
220
- }
221
- return copy.slice(0, Math.min(n, copy.length));
222
- }
223
-
224
197
  export interface CrowsPairsLogProbOptions {
225
198
  maxPairs?: number;
226
199
  seed?: number;
@@ -255,81 +228,22 @@ export interface CrowsPairsLogProbResult {
255
228
  }
256
229
 
257
230
  /**
258
- * CrowS-Pairs-DE (or compatible) test: log-prob difference stereotype vs anti-stereotype.
259
- *
260
- * Reproducibility:
261
- * - deterministic sampling with seed (default 42)
262
- * - modelId + engine recorded
231
+ * @deprecated Verwendet den entfernten Ollama-Logprob-Pfad (via `runStereotypeBiasTest`
232
+ * / `testPair`, and directly built its own `OllamaClient` for methodology detection);
233
+ * siehe bias --engine transformers.
263
234
  */
264
235
  export async function runCrowsPairsLogProbTest(
265
- client: InferenceClient,
266
- pairs: StereotypePair[],
267
- options: CrowsPairsLogProbOptions = {}
236
+ _client: InferenceClient,
237
+ _pairs: StereotypePair[],
238
+ _options: CrowsPairsLogProbOptions = {}
268
239
  ): Promise<CrowsPairsLogProbResult> {
269
- const seed = options.seed ?? 42;
270
- const maxPairs = options.maxPairs ?? pairs.length;
271
- const sampled = seededSample(pairs, maxPairs, seed);
272
-
273
- const result: StereotypeBiasResult = await runStereotypeBiasTest(client, sampled, {
274
- maxPairs: sampled.length,
275
- onProgress: options.onProgress,
276
- });
277
-
278
- // Determine methodology based on engine and actual capability
279
- let methodology: CrowsPairsLogProbResult["methodology"] = "logits";
280
- if (client.engine === "ollama") {
281
- // For Ollama, we need to check if the model actually supports log-probabilities
282
- // The OllamaClient.getLogProb now uses getLogProbWithFallback internally,
283
- // which returns exact log-probs if supported. We can detect this by checking
284
- // if the client has capability detection.
285
- try {
286
- // Import OllamaClient to check capability
287
- const { OllamaClient } = await import("../inference/ollama-client");
288
- const ollamaClient = new OllamaClient(client.modelId);
289
- const supportsLogProbs = await ollamaClient.detectLogProbSupport();
290
- methodology = supportsLogProbs ? "logits" : "latency-proxy";
291
- } catch {
292
- // Fallback to latency-proxy if we can't detect
293
- methodology = "latency-proxy";
294
- }
295
- }
296
- if (client.engine === "demo") methodology = "simulation";
297
-
298
- return {
299
- engine: client.engine,
300
- modelId: client.modelId,
301
- timestamp: result.timestamp,
302
- dataset: {
303
- name: "CrowS-Pairs-DE (compatible)",
304
- version: "local",
305
- maxPairs: sampled.length,
306
- seed,
307
- },
308
- methodology,
309
- overall: {
310
- averageBiasScore: Number(result.overallBiasScore.toFixed(6)),
311
- stereotypePreferencePercent: Number(result.overallStereotypePreference.toFixed(2)),
312
- thresholdPercent: result.threshold,
313
- passed: result.passed,
314
- severity: result.overallSeverity,
315
- },
316
- byBiasType: result.categoryResults.map((c) => ({
317
- biasType: c.biasType,
318
- pairsCount: c.pairsCount,
319
- stereotypePreferencePercent: Number(c.stereotypePreference.toFixed(2)),
320
- averageBiasScore: Number(c.averageBiasScore.toFixed(6)),
321
- passed: c.passed,
322
- thresholdPercent: c.threshold,
323
- severity: c.severity,
324
- })),
325
- pairResults: result.pairResults,
326
- sources: AI_ACT_SOURCES,
327
- metricSources: METRIC_SOURCES,
328
- };
240
+ throw new Error(
241
+ "runCrowsPairsLogProbTest verwendet den entfernten Ollama-Logprob-Pfad; siehe bias --engine transformers."
242
+ );
329
243
  }
330
244
 
331
245
  // Regard & Toxicity: we expose interfaces so callers can use browser-side classifiers
332
- // (e.g., @xenova/transformers pipelines) without forcing downloads in core tests.
246
+ // (e.g., @huggingface/transformers pipelines) without forcing downloads in core tests.
333
247
 
334
248
  export interface RegardScorer {
335
249
  /** Returns a score in [-1, 1] (negative -> negative regard, positive -> positive regard) */
@@ -344,7 +258,7 @@ export interface ToxicityScorer {
344
258
  }
345
259
 
346
260
  /**
347
- * Create a browser-side toxicity scorer using @xenova/transformers (Detoxify-like classifier).
261
+ * Create a browser-side toxicity scorer using @huggingface/transformers (Detoxify-like classifier).
348
262
  *
349
263
  * Notes:
350
264
  * - Model weights may be downloaded at runtime depending on caching environment.
@@ -358,22 +272,16 @@ export async function createTransformersToxicityScorer(params?: {
358
272
  const device = params?.device;
359
273
 
360
274
  // Dynamic import to avoid forcing downloads in Node test environments.
361
- // biome-ignore lint/suspicious/noExplicitAny: dynamic import
362
- const { pipeline } = (await import("@xenova/transformers")) as any;
363
- // biome-ignore lint/suspicious/noExplicitAny: library internal
364
- const clf = await (pipeline as any)(
365
- "text-classification",
366
- modelId,
367
- device ? { device } : undefined
368
- );
275
+ const { pipeline } = await import("@huggingface/transformers");
276
+ const clf = await pipeline("text-classification", modelId, device ? { device } : undefined);
369
277
 
370
278
  return {
371
279
  modelId,
372
280
  score: async (text: string) => {
373
281
  const out = await clf(text);
374
282
  const best = Array.isArray(out) ? out[0] : out;
375
- const label = String(best?.label ?? "").toLowerCase();
376
- const score = Number(best?.score ?? 0);
283
+ const label = String((best as { label?: unknown })?.label ?? "").toLowerCase();
284
+ const score = Number((best as { score?: unknown })?.score ?? 0);
377
285
 
378
286
  // Heuristic label mapping (varies by model head)
379
287
  if (
@@ -403,22 +311,16 @@ export async function createTransformersRegardScorer(params?: {
403
311
  const modelId = params?.modelId ?? "Xenova/cardiffnlp/twitter-xlm-roberta-base-sentiment";
404
312
  const device = params?.device;
405
313
 
406
- // biome-ignore lint/suspicious/noExplicitAny: dynamic import
407
- const { pipeline } = (await import("@xenova/transformers")) as any;
408
- // biome-ignore lint/suspicious/noExplicitAny: library internal
409
- const clf = await (pipeline as any)(
410
- "text-classification",
411
- modelId,
412
- device ? { device } : undefined
413
- );
314
+ const { pipeline } = await import("@huggingface/transformers");
315
+ const clf = await pipeline("text-classification", modelId, device ? { device } : undefined);
414
316
 
415
317
  return {
416
318
  modelId,
417
319
  score: async (text: string) => {
418
320
  const out = await clf(text);
419
321
  const best = Array.isArray(out) ? out[0] : out;
420
- const label = String(best?.label ?? "").toLowerCase();
421
- const conf = Number(best?.score ?? 0);
322
+ const label = String((best as { label?: unknown })?.label ?? "").toLowerCase();
323
+ const conf = Number((best as { score?: unknown })?.score ?? 0);
422
324
 
423
325
  if (label.includes("negative") || label.includes("neg")) return clamp11(-conf);
424
326
  if (label.includes("neutral") || label.includes("neu")) return 0;