@struktur/benchmarks 2.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 (51) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +108 -0
  3. package/dist/cache.d.ts +5 -0
  4. package/dist/cache.d.ts.map +1 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +961 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/config.d.ts +13 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/datasets/index.d.ts +5 -0
  12. package/dist/datasets/index.d.ts.map +1 -0
  13. package/dist/datasets/longdoc.d.ts +34 -0
  14. package/dist/datasets/longdoc.d.ts.map +1 -0
  15. package/dist/datasets/source.d.ts +48 -0
  16. package/dist/datasets/source.d.ts.map +1 -0
  17. package/dist/datasets/sroie.d.ts +14 -0
  18. package/dist/datasets/sroie.d.ts.map +1 -0
  19. package/dist/datasets/synthetic.d.ts +27 -0
  20. package/dist/datasets/synthetic.d.ts.map +1 -0
  21. package/dist/datasets.js +392 -0
  22. package/dist/datasets.js.map +1 -0
  23. package/dist/expose/gold-score.d.ts +15 -0
  24. package/dist/expose/gold-score.d.ts.map +1 -0
  25. package/dist/expose/gold.d.ts +8 -0
  26. package/dist/expose/gold.d.ts.map +1 -0
  27. package/dist/expose/instructions.d.ts +2 -0
  28. package/dist/expose/instructions.d.ts.map +1 -0
  29. package/dist/expose/metric.d.ts +7 -0
  30. package/dist/expose/metric.d.ts.map +1 -0
  31. package/dist/expose/schema.d.ts +304 -0
  32. package/dist/expose/schema.d.ts.map +1 -0
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +686 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/pricing.d.ts +15 -0
  38. package/dist/pricing.d.ts.map +1 -0
  39. package/dist/report.d.ts +6 -0
  40. package/dist/report.d.ts.map +1 -0
  41. package/dist/runner.d.ts +81 -0
  42. package/dist/runner.d.ts.map +1 -0
  43. package/dist/scoring/normalize.d.ts +20 -0
  44. package/dist/scoring/normalize.d.ts.map +1 -0
  45. package/dist/scoring/score.d.ts +39 -0
  46. package/dist/scoring/score.d.ts.map +1 -0
  47. package/dist/tracks.d.ts +16 -0
  48. package/dist/tracks.d.ts.map +1 -0
  49. package/dist/types.d.ts +88 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/package.json +40 -0
package/dist/cli.js ADDED
@@ -0,0 +1,961 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/config.ts
13
+ var config_exports = {};
14
+ __export(config_exports, {
15
+ BENCHMARK_MODEL: () => BENCHMARK_MODEL,
16
+ defaultDatasetDir: () => defaultDatasetDir,
17
+ defaultResultsDir: () => defaultResultsDir
18
+ });
19
+ var BENCHMARK_MODEL, defaultDatasetDir, defaultResultsDir;
20
+ var init_config = __esm({
21
+ "src/config.ts"() {
22
+ "use strict";
23
+ BENCHMARK_MODEL = "openrouter/deepseek/deepseek-v4-flash-vision-exp";
24
+ defaultDatasetDir = () => process.env.STRUKTUR_BENCHMARKS_DIR ?? `${process.env.HOME ?? "."}/.struktur/benchmarks`;
25
+ defaultResultsDir = () => `${defaultDatasetDir()}/results`;
26
+ }
27
+ });
28
+
29
+ // src/cli.ts
30
+ import { resolve } from "path";
31
+
32
+ // src/datasets/source.ts
33
+ init_config();
34
+ import { mkdir, readFile, writeFile } from "fs/promises";
35
+ import path from "path";
36
+ var readCachedCases = async (file) => {
37
+ try {
38
+ return JSON.parse(await readFile(file, "utf8"));
39
+ } catch {
40
+ return void 0;
41
+ }
42
+ };
43
+ async function loadDataset(dataset, options = {}) {
44
+ const base = path.join(options.cacheDir ?? defaultDatasetDir(), dataset.id, dataset.version);
45
+ const casesFile = path.join(base, "cases.json");
46
+ const rawDir = path.join(base, "raw");
47
+ const limit = options.limit;
48
+ const offset = options.offset ?? 0;
49
+ let cases = options.force ? void 0 : await readCachedCases(casesFile);
50
+ if (!cases) {
51
+ if (dataset.source.kind === "manual") {
52
+ const hint = dataset.source.hint ?? `download the dataset into ${rawDir}`;
53
+ throw new Error(`Dataset "${dataset.id}" requires manual download. ${hint}`);
54
+ }
55
+ await mkdir(rawDir, { recursive: true });
56
+ await dataset.download(rawDir, options.fetchImpl);
57
+ cases = await dataset.convert(rawDir);
58
+ await mkdir(base, { recursive: true });
59
+ await writeFile(casesFile, JSON.stringify(cases, null, 2), "utf8");
60
+ await writeFile(
61
+ path.join(base, "manifest.json"),
62
+ JSON.stringify(
63
+ {
64
+ id: dataset.id,
65
+ version: dataset.version,
66
+ license: dataset.license,
67
+ attribution: dataset.attribution,
68
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
69
+ count: cases.length
70
+ },
71
+ null,
72
+ 2
73
+ ),
74
+ "utf8"
75
+ );
76
+ }
77
+ return cases.slice(offset, limit !== void 0 ? offset + limit : void 0);
78
+ }
79
+ var DEFAULT_HUB_BASE = "https://datasets-server.huggingface.co";
80
+ async function downloadHubRows(source, fetchImpl = fetch) {
81
+ const base = source.baseUrl ?? DEFAULT_HUB_BASE;
82
+ const repo = encodeURIComponent(source.repo);
83
+ const config = source.config ?? "default";
84
+ const split = source.split ?? "train";
85
+ const pageSize = 100;
86
+ const rows = [];
87
+ let offset = 0;
88
+ for (; ; ) {
89
+ const url = `${base}/rows?dataset=${repo}&config=${config}&split=${split}&offset=${offset}&length=${pageSize}`;
90
+ const res = await fetchImpl(url);
91
+ if (!res.ok) {
92
+ throw new Error(`Hugging Face datasets-server error ${res.status} for ${url}`);
93
+ }
94
+ const body = await res.json();
95
+ const page = body.rows ?? [];
96
+ if (page.length === 0) break;
97
+ rows.push(...page.map((r) => r.row));
98
+ if (page.length < pageSize) break;
99
+ offset += page.length;
100
+ }
101
+ return rows;
102
+ }
103
+
104
+ // src/datasets/sroie.ts
105
+ var REPO = "darentang/sroie";
106
+ var CONFIG = "sroie";
107
+ var LABELS = [
108
+ "O",
109
+ "B-COMPANY",
110
+ "I-COMPANY",
111
+ "B-DATE",
112
+ "I-DATE",
113
+ "B-ADDRESS",
114
+ "I-ADDRESS",
115
+ "B-TOTAL",
116
+ "I-TOTAL"
117
+ ];
118
+ var SCHEMA = {
119
+ type: "object",
120
+ properties: {
121
+ company: { type: ["string", "null"] },
122
+ date: { type: ["string", "null"] },
123
+ address: { type: ["string", "null"] },
124
+ total: { type: ["string", "null"] }
125
+ },
126
+ additionalProperties: false
127
+ };
128
+ function reconstructEntities(words, tags) {
129
+ const out = {
130
+ company: null,
131
+ date: null,
132
+ address: null,
133
+ total: null
134
+ };
135
+ let current = null;
136
+ let buffer = [];
137
+ const flush = () => {
138
+ if (current && buffer.length > 0) {
139
+ out[current.toLowerCase()] = buffer.join(" ");
140
+ }
141
+ current = null;
142
+ buffer = [];
143
+ };
144
+ for (let i = 0; i < words.length; i++) {
145
+ const tag = LABELS[tags[i] ?? 0] ?? "O";
146
+ if (tag.startsWith("B-")) {
147
+ flush();
148
+ current = tag.slice(2);
149
+ buffer = [words[i] ?? ""];
150
+ } else if (tag.startsWith("I-") && current === tag.slice(2)) {
151
+ buffer.push(words[i] ?? "");
152
+ } else {
153
+ flush();
154
+ }
155
+ }
156
+ flush();
157
+ return out;
158
+ }
159
+ var imageUrl = (imagePath) => `https://huggingface.co/datasets/${REPO}/resolve/main/${imagePath}`;
160
+ var imageArtifact = (id, url) => ({
161
+ id: `artifact-${id}`,
162
+ type: "image",
163
+ raw: async () => Buffer.from(""),
164
+ contents: [{ media: [{ type: "image", url }] }]
165
+ });
166
+ function sroieToCases(rows) {
167
+ const cases = [];
168
+ for (const row of rows) {
169
+ const words = row.words ?? [];
170
+ const tags = row.ner_tags ?? [];
171
+ const imagePath = row.image_path;
172
+ if (!imagePath) continue;
173
+ const gold = reconstructEntities(words, tags);
174
+ const id = row.id ?? imagePath;
175
+ const url = imageUrl(imagePath);
176
+ const text = words.join(" ");
177
+ cases.push({
178
+ id: `sroie-${id}`,
179
+ schema: SCHEMA,
180
+ gold,
181
+ artifacts: [imageArtifact(id, url)],
182
+ tracks: ["text", "text+embedded"],
183
+ artifactsByTrack: {
184
+ text: [
185
+ {
186
+ id: `artifact-${id}-text`,
187
+ type: "text",
188
+ raw: async () => Buffer.from(text, "utf8"),
189
+ contents: [{ text }]
190
+ }
191
+ ],
192
+ "text+embedded": [imageArtifact(id, url)]
193
+ },
194
+ metrics: { default: "normalized" },
195
+ source: {
196
+ dataset: "sroie",
197
+ license: "ICDAR 2019 SROIE (research use)",
198
+ url: "https://github.com/zzzDavid/ICDAR-2019-SROIE",
199
+ attribution: "ICDAR 2019 SROIE (Huang et al., 2019)"
200
+ }
201
+ });
202
+ }
203
+ return cases;
204
+ }
205
+ var download = async (rawDir, fetchImpl) => {
206
+ const { writeFile: writeFile4 } = await import("fs/promises");
207
+ const path4 = await import("path");
208
+ const all = [];
209
+ for (const split of ["train", "test"]) {
210
+ const rows = await downloadHubRows(
211
+ { kind: "hub", repo: REPO, config: CONFIG, split },
212
+ fetchImpl
213
+ );
214
+ all.push(...rows);
215
+ }
216
+ await writeFile4(path4.join(rawDir, "rows.json"), JSON.stringify(all), "utf8");
217
+ };
218
+ var convert = async (rawDir) => {
219
+ const { readFile: readFile3 } = await import("fs/promises");
220
+ const path4 = await import("path");
221
+ const rows = JSON.parse(await readFile3(path4.join(rawDir, "rows.json"), "utf8"));
222
+ return sroieToCases(rows);
223
+ };
224
+ var sroie = {
225
+ id: "sroie",
226
+ version: "1",
227
+ source: { kind: "hub", repo: REPO, config: CONFIG },
228
+ license: "ICDAR 2019 SROIE (research use)",
229
+ attribution: "ICDAR 2019 SROIE (Huang et al., 2019)",
230
+ download,
231
+ convert
232
+ };
233
+
234
+ // src/types.ts
235
+ var TRACKS = [
236
+ "text",
237
+ "text+embedded",
238
+ "text+screenshots",
239
+ "text+embedded+screenshots"
240
+ ];
241
+
242
+ // src/index.ts
243
+ init_config();
244
+
245
+ // src/pricing.ts
246
+ var PRICING = {
247
+ // OpenRouter: $0.22/M input, $0.66/M output
248
+ "openrouter/deepseek/deepseek-v4-flash-vision-exp": { input: 0.22, output: 0.66 },
249
+ // OpenAI via OpenRouter: $0.15/M input, $0.60/M output
250
+ "openrouter/openai/gpt-4o-mini": { input: 0.15, output: 0.6 }
251
+ };
252
+ var envOverride = (() => {
253
+ const raw = process.env.STRUKTUR_BENCHMARK_PRICES;
254
+ if (!raw) return void 0;
255
+ try {
256
+ return JSON.parse(raw);
257
+ } catch {
258
+ return void 0;
259
+ }
260
+ })();
261
+ var normalizeModel = (model) => model.replace(/#.*$/, "");
262
+ function pricingFor(model) {
263
+ const key = normalizeModel(model);
264
+ return envOverride?.[key] ?? PRICING[key];
265
+ }
266
+ function estimateCostUsd(model, usage) {
267
+ const p = pricingFor(model);
268
+ if (!p) return 0;
269
+ return usage.inputTokens / 1e6 * p.input + usage.outputTokens / 1e6 * p.output;
270
+ }
271
+
272
+ // src/tracks.ts
273
+ import { parsePdf } from "@struktur/sdk";
274
+ var trackToPdfOptions = (track) => {
275
+ switch (track) {
276
+ case "text":
277
+ return { includeImages: false, screenshots: false };
278
+ case "text+embedded":
279
+ return { includeImages: true, screenshots: false };
280
+ case "text+screenshots":
281
+ return { includeImages: false, screenshots: true };
282
+ case "text+embedded+screenshots":
283
+ return { includeImages: true, screenshots: true };
284
+ }
285
+ };
286
+ async function materializeTrack(artifacts, track) {
287
+ return Promise.all(
288
+ artifacts.map(async (artifact) => {
289
+ if (artifact.type !== "pdf") return artifact;
290
+ const buffer = await artifact.raw();
291
+ return parsePdf(buffer, trackToPdfOptions(track));
292
+ })
293
+ );
294
+ }
295
+
296
+ // src/scoring/normalize.ts
297
+ var EMPTY = /* @__PURE__ */ Symbol("struktur.benchmarks.empty");
298
+ var stripDiacritics = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
299
+ var normalizeString = (s) => stripDiacritics(s.toLowerCase()).replace(/\s+/g, " ").trim();
300
+ var normalizeAggressive = (s) => stripDiacritics(s.toLowerCase()).replace(/[^a-z0-9]+/g, "");
301
+ function normalizeValue(value, metric) {
302
+ if (value === null || value === void 0) return EMPTY;
303
+ if (metric === "exact") {
304
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
305
+ return value;
306
+ }
307
+ return JSON.stringify(value);
308
+ }
309
+ if (metric === "semantic") {
310
+ if (typeof value === "number" || typeof value === "boolean") return value;
311
+ return normalizeAggressive(typeof value === "string" ? value : JSON.stringify(value));
312
+ }
313
+ if (typeof value === "number" || typeof value === "boolean") return value;
314
+ return normalizeString(typeof value === "string" ? value : JSON.stringify(value));
315
+ }
316
+ var RELATIVE_TOLERANCE = 1e-6;
317
+ function valuesEqual(a, b, metric) {
318
+ const na = normalizeValue(a, metric);
319
+ const nb = normalizeValue(b, metric);
320
+ if (na === EMPTY || nb === EMPTY) return na === EMPTY && nb === EMPTY;
321
+ if (metric === "tolerance" && typeof na === "number" && typeof nb === "number") {
322
+ const scale = Math.max(1, Math.abs(na), Math.abs(nb));
323
+ return Math.abs(na - nb) <= RELATIVE_TOLERANCE * scale;
324
+ }
325
+ return na === nb;
326
+ }
327
+
328
+ // src/scoring/score.ts
329
+ var STOPWORDS = /* @__PURE__ */ new Set([
330
+ "the",
331
+ "a",
332
+ "an",
333
+ "and",
334
+ "or",
335
+ "of",
336
+ "to",
337
+ "in",
338
+ "on",
339
+ "at",
340
+ "is",
341
+ "are",
342
+ "it",
343
+ "this",
344
+ "that",
345
+ "with",
346
+ "for",
347
+ "as",
348
+ "by",
349
+ "from",
350
+ "be",
351
+ "was",
352
+ "were",
353
+ "und",
354
+ "der",
355
+ "die",
356
+ "das",
357
+ "ein",
358
+ "eine",
359
+ "einen",
360
+ "dem",
361
+ "den",
362
+ "mit",
363
+ "von",
364
+ "zur",
365
+ "zum",
366
+ "auf",
367
+ "im",
368
+ "in",
369
+ "am",
370
+ "ist",
371
+ "sind",
372
+ "werden",
373
+ "wird",
374
+ "des",
375
+ "sich",
376
+ "auch",
377
+ "nicht",
378
+ "als",
379
+ "bei",
380
+ "f\xFCr",
381
+ "\xFCber",
382
+ "aus",
383
+ "nach",
384
+ "einer",
385
+ "einem",
386
+ "eine",
387
+ "zu",
388
+ "unter",
389
+ "an",
390
+ "sowie",
391
+ "durch",
392
+ "alle",
393
+ "dass",
394
+ "dieser",
395
+ "diese",
396
+ "hier",
397
+ "sein",
398
+ "ihr",
399
+ "wir",
400
+ "man"
401
+ ]);
402
+ var tokenizeProse = (s) => {
403
+ if (typeof s !== "string") return [];
404
+ return Array.from(
405
+ new Set(
406
+ s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w.length >= 3 && !STOPWORDS.has(w))
407
+ )
408
+ );
409
+ };
410
+ function scoreProse(gold, pred) {
411
+ const s = empty();
412
+ const gt = tokenizeProse(String(gold ?? ""));
413
+ const pt = tokenizeProse(String(pred ?? ""));
414
+ const gs = new Set(gt);
415
+ const ps = new Set(pt);
416
+ const tp = gt.filter((w) => ps.has(w)).length;
417
+ const fp = pt.filter((w) => !gs.has(w)).length;
418
+ const fn = gt.filter((w) => !ps.has(w)).length;
419
+ s.tp = tp;
420
+ s.fp = fp;
421
+ s.fn = fn;
422
+ s.totalGold = gt.length;
423
+ s.totalPred = pt.length;
424
+ return finalize(s);
425
+ }
426
+ var empty = () => ({
427
+ tp: 0,
428
+ fp: 0,
429
+ fn: 0,
430
+ totalGold: 0,
431
+ totalPred: 0,
432
+ precision: 1,
433
+ recall: 1,
434
+ f1: 1,
435
+ exactMatch: true,
436
+ fieldErrors: []
437
+ });
438
+ var finalize = (s) => {
439
+ s.precision = s.tp + s.fp === 0 ? 1 : s.tp / (s.tp + s.fp);
440
+ s.recall = s.tp + s.fn === 0 ? 1 : s.tp / (s.tp + s.fn);
441
+ s.f1 = s.precision + s.recall === 0 ? 0 : 2 * s.precision * s.recall / (s.precision + s.recall);
442
+ s.exactMatch = s.fp === 0 && s.fn === 0;
443
+ return s;
444
+ };
445
+ var merge = (into, child) => {
446
+ into.tp += child.tp;
447
+ into.fp += child.fp;
448
+ into.fn += child.fn;
449
+ into.totalGold += child.totalGold;
450
+ into.totalPred += child.totalPred;
451
+ into.fieldErrors.push(...child.fieldErrors);
452
+ };
453
+ var isLeaf = (v) => v === null || v === void 0 || typeof v === "string" || typeof v === "number" || typeof v === "boolean";
454
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
455
+ var leafCount = (v) => {
456
+ if (Array.isArray(v)) return v.reduce((n, item) => n + leafCount(item), 0);
457
+ if (isObject(v)) {
458
+ return Object.values(v).reduce((n, item) => n + leafCount(item), 0);
459
+ }
460
+ return 1;
461
+ };
462
+ var canonPath = (path4) => path4.replace(/\[[^\]]*\]/g, "[]");
463
+ var metricFor = (spec, path4) => (
464
+ // field overrides are keyed by the leaf field name (e.g. "area"), robust to
465
+ // concrete array keys in the path.
466
+ spec.fields?.[path4.split(".").pop()] ?? spec.default ?? "normalized"
467
+ );
468
+ var alignmentFor = (spec, path4) => spec.arrays?.find((a) => canonPath(a.path) === canonPath(path4)) ?? spec.arrays?.find((a) => canonPath(path4).endsWith(canonPath(a.path)));
469
+ var isSetPath = (spec, path4) => (spec.sets ?? []).some(
470
+ (s) => canonPath(s) === canonPath(path4) || canonPath(path4).endsWith(canonPath(s))
471
+ );
472
+ function groupByKey(items, key) {
473
+ const map = /* @__PURE__ */ new Map();
474
+ const keys = Array.isArray(key) ? key : [key];
475
+ for (const item of items) {
476
+ const k = keys.map((kf) => isObject(item) ? item[kf] : void 0).map((v) => String(normalizeValue(v, "normalized"))).join("|");
477
+ const arr = map.get(k) ?? [];
478
+ arr.push(item);
479
+ map.set(k, arr);
480
+ }
481
+ return map;
482
+ }
483
+ function scoreLeaf(path4, gold, pred, spec) {
484
+ const metric = metricFor(spec, path4);
485
+ if (metric === "prose" && (typeof gold === "string" || typeof pred === "string")) {
486
+ return scoreProse(gold, pred);
487
+ }
488
+ const s = empty();
489
+ s.totalGold = isLeaf(gold) ? 1 : leafCount(gold);
490
+ s.totalPred = isLeaf(pred) ? 1 : leafCount(pred);
491
+ if (valuesEqual(gold, pred, metric)) {
492
+ s.tp = isLeaf(gold) ? 1 : leafCount(gold);
493
+ return finalize(s);
494
+ }
495
+ const goldEmpty = gold === null || gold === void 0;
496
+ const predEmpty = pred === null || pred === void 0;
497
+ if (goldEmpty) s.fp = s.totalPred;
498
+ else if (predEmpty) s.fn = s.totalGold;
499
+ else {
500
+ s.fn = s.totalGold;
501
+ s.fp = s.totalPred;
502
+ }
503
+ s.fieldErrors.push({ path: path4, gold, pred, metric });
504
+ return finalize(s);
505
+ }
506
+ function scoreIndexed(path4, g, p, spec) {
507
+ const s = empty();
508
+ const len = Math.max(g.length, p.length);
509
+ for (let i = 0; i < len; i++) {
510
+ merge(s, scoreAt(`${path4}[${i}]`, g[i] ?? null, p[i] ?? null, spec));
511
+ }
512
+ return finalize(s);
513
+ }
514
+ function scoreAligned(path4, g, p, key, spec) {
515
+ const s = empty();
516
+ const gm = groupByKey(g, key);
517
+ const pm = groupByKey(p, key);
518
+ let matchedAny = false;
519
+ for (const [k, gItems] of gm) {
520
+ const pItems = pm.get(k);
521
+ if (!pItems || pItems.length === 0) {
522
+ for (const gi of gItems) {
523
+ s.fn += leafCount(gi);
524
+ s.totalGold += leafCount(gi);
525
+ }
526
+ continue;
527
+ }
528
+ matchedAny = true;
529
+ const pairs = Math.min(gItems.length, pItems.length);
530
+ for (let i = 0; i < pairs; i++) {
531
+ merge(s, scoreAt(`${path4}[${String(k)}]`, gItems[i], pItems[i], spec));
532
+ }
533
+ for (let i = pairs; i < gItems.length; i++) {
534
+ s.fn += leafCount(gItems[i]);
535
+ s.totalGold += leafCount(gItems[i]);
536
+ }
537
+ for (let i = pairs; i < pItems.length; i++) {
538
+ s.fp += leafCount(pItems[i]);
539
+ s.totalPred += leafCount(pItems[i]);
540
+ }
541
+ }
542
+ for (const [k, pItems] of pm) {
543
+ if (gm.has(k)) continue;
544
+ for (const pi of pItems) {
545
+ s.fp += leafCount(pi);
546
+ s.totalPred += leafCount(pi);
547
+ }
548
+ }
549
+ if (!matchedAny && g.length > 0 && p.length > 0) {
550
+ return scoreIndexed(path4, g, p, spec);
551
+ }
552
+ return finalize(s);
553
+ }
554
+ function scoreSet(path4, g, p, spec) {
555
+ const s = empty();
556
+ const goldKeyed = /* @__PURE__ */ new Map();
557
+ const predKeyed = /* @__PURE__ */ new Map();
558
+ const metric = metricFor(spec, canonPath(path4));
559
+ const bump = (map, v) => {
560
+ const k = String(normalizeValue(v, metric));
561
+ map.set(k, (map.get(k) ?? 0) + 1);
562
+ };
563
+ for (const v of g) bump(goldKeyed, v);
564
+ for (const v of p) bump(predKeyed, v);
565
+ s.totalGold = g.length;
566
+ s.totalPred = p.length;
567
+ for (const [k, gc] of goldKeyed) {
568
+ const pc = predKeyed.get(k) ?? 0;
569
+ const common = Math.min(gc, pc);
570
+ s.tp += common;
571
+ s.fn += gc - common;
572
+ s.fp += pc - common;
573
+ if (common < gc) {
574
+ s.fieldErrors.push({
575
+ path: path4,
576
+ gold: g.find((v) => String(normalizeValue(v, metric)) === k),
577
+ pred: null,
578
+ metric
579
+ });
580
+ }
581
+ }
582
+ for (const [k, pc] of predKeyed) {
583
+ const gc = goldKeyed.get(k) ?? 0;
584
+ if (pc > gc) {
585
+ s.fieldErrors.push({
586
+ path: path4,
587
+ gold: null,
588
+ pred: p.find((v) => String(normalizeValue(v, metric)) === k),
589
+ metric
590
+ });
591
+ }
592
+ }
593
+ return finalize(s);
594
+ }
595
+ function scoreAt(path4, gold, pred, spec) {
596
+ if (isLeaf(gold) || isLeaf(pred)) {
597
+ return scoreLeaf(path4, gold, pred, spec);
598
+ }
599
+ if (Array.isArray(gold) || Array.isArray(pred)) {
600
+ const g = Array.isArray(gold) ? gold : [gold];
601
+ const p = Array.isArray(pred) ? pred : [pred];
602
+ if (isSetPath(spec, path4)) {
603
+ return scoreSet(path4, g, p, spec);
604
+ }
605
+ const align = alignmentFor(spec, path4);
606
+ return align ? scoreAligned(path4, g, p, align.key, spec) : scoreIndexed(path4, g, p, spec);
607
+ }
608
+ const s = empty();
609
+ const keys = /* @__PURE__ */ new Set([...Object.keys(gold), ...Object.keys(pred)]);
610
+ for (const key of keys) {
611
+ const childPath = path4 === "" ? key : `${path4}.${key}`;
612
+ const gv = gold[key] ?? null;
613
+ const pv = pred[key] ?? null;
614
+ merge(s, scoreAt(childPath, gv, pv, spec));
615
+ }
616
+ return finalize(s);
617
+ }
618
+ function scoreData(gold, pred, spec = {}) {
619
+ return scoreAt("", gold, pred, spec);
620
+ }
621
+
622
+ // src/runner.ts
623
+ import { extract, resolveModel, toJsonSchema } from "@struktur/sdk";
624
+ import {
625
+ simple,
626
+ parallel,
627
+ sequential,
628
+ parallelAutoMerge,
629
+ sequentialAutoMerge,
630
+ doublePass,
631
+ doublePassAutoMerge,
632
+ agent
633
+ } from "@struktur/sdk";
634
+
635
+ // src/cache.ts
636
+ import { createHash } from "crypto";
637
+ import { mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "fs/promises";
638
+ import path2 from "path";
639
+ var stableStringify = (v) => {
640
+ if (v === null) return "null";
641
+ if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`;
642
+ if (typeof v === "object") {
643
+ const rec = v;
644
+ const keys = Object.keys(rec).sort();
645
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(rec[k])}`).join(",")}}`;
646
+ }
647
+ return JSON.stringify(v) ?? String(v);
648
+ };
649
+ var hashKey = (input) => createHash("sha256").update(stableStringify(input)).digest("hex").slice(0, 16);
650
+ async function cacheGet(dir, key) {
651
+ try {
652
+ const raw = await readFile2(path2.join(dir, `${key}.json`), "utf8");
653
+ return JSON.parse(raw);
654
+ } catch {
655
+ return void 0;
656
+ }
657
+ }
658
+ async function cacheSet(dir, key, value) {
659
+ const file = path2.join(dir, `${key}.json`);
660
+ await mkdir2(dir, { recursive: true });
661
+ const tmp = `${file}.tmp`;
662
+ await writeFile2(tmp, JSON.stringify(value, null, 2), "utf8");
663
+ await rename(tmp, file);
664
+ }
665
+
666
+ // src/runner.ts
667
+ init_config();
668
+ var builtins = {
669
+ simple: (model, _spec, instructions) => simple({ model, outputInstructions: instructions }),
670
+ parallel: (model, _spec, instructions) => parallel({ model, mergeModel: model, chunkSize: 1e4, outputInstructions: instructions }),
671
+ sequential: (model, _spec, instructions) => sequential({ model, chunkSize: 1e4, outputInstructions: instructions }),
672
+ parallelAutoMerge: (model, _spec, instructions) => parallelAutoMerge({
673
+ model,
674
+ dedupeModel: model,
675
+ chunkSize: 1e4,
676
+ outputInstructions: instructions
677
+ }),
678
+ sequentialAutoMerge: (model, _spec, instructions) => sequentialAutoMerge({
679
+ model,
680
+ dedupeModel: model,
681
+ chunkSize: 1e4,
682
+ outputInstructions: instructions
683
+ }),
684
+ doublePass: (model, _spec, instructions) => doublePass({ model, mergeModel: model, chunkSize: 1e4, outputInstructions: instructions }),
685
+ doublePassAutoMerge: (model, _spec, instructions) => doublePassAutoMerge({
686
+ model,
687
+ dedupeModel: model,
688
+ chunkSize: 1e4,
689
+ outputInstructions: instructions
690
+ }),
691
+ agent: (_model, modelSpec, instructions) => {
692
+ const [provider, ...rest] = modelSpec.split("/");
693
+ const modelId = rest.join("/");
694
+ if (!provider || !modelId)
695
+ throw new Error(`Agent requires 'provider/model'. Got: ${modelSpec}`);
696
+ return agent({
697
+ provider,
698
+ modelId,
699
+ maxSteps: 50,
700
+ maxIterations: 1,
701
+ vision: true,
702
+ outputInstructions: instructions
703
+ });
704
+ }
705
+ };
706
+ function resolveStrategy(entry, model, modelSpec) {
707
+ if (typeof entry === "string") {
708
+ const factory = builtins[entry];
709
+ if (!factory) {
710
+ throw new Error(
711
+ `Unknown builtin strategy: ${entry}. Available: ${Object.keys(builtins).join(", ")}`
712
+ );
713
+ }
714
+ return { name: entry, strategy: factory(model, modelSpec) };
715
+ }
716
+ if (typeof entry === "function") {
717
+ const s = entry(model, modelSpec);
718
+ return { name: s.name, strategy: s };
719
+ }
720
+ if ("strategy" in entry) {
721
+ const inner = entry.strategy;
722
+ if (typeof inner === "string") {
723
+ const factory = builtins[inner];
724
+ if (!factory) throw new Error(`Unknown builtin strategy: ${inner}`);
725
+ return {
726
+ name: entry.label ?? inner,
727
+ strategy: factory(model, modelSpec, entry.instructions)
728
+ };
729
+ }
730
+ const s = inner(model, modelSpec, entry.instructions);
731
+ return { name: entry.label ?? s.name, strategy: s };
732
+ }
733
+ return {
734
+ name: entry.name,
735
+ strategy: entry
736
+ };
737
+ }
738
+ var emptyUsage = () => ({ inputTokens: 0, outputTokens: 0, totalTokens: 0 });
739
+ async function runBenchmark(options) {
740
+ const modelSpec = options.model ?? BENCHMARK_MODEL;
741
+ const model = await resolveModel(modelSpec);
742
+ const cacheDir = options.cacheDir ?? defaultResultsDir();
743
+ const useCache = options.cache ?? true;
744
+ const variant = options.variant ?? "";
745
+ const strats = options.strategies.map((s) => resolveStrategy(s, model, modelSpec));
746
+ const cells = [];
747
+ for (const c of options.cases) {
748
+ const tracks = c.tracks ?? [...TRACKS];
749
+ for (const track of tracks) {
750
+ for (const { name, strategy } of strats) {
751
+ const cacheKey = hashKey({
752
+ caseId: c.id,
753
+ track,
754
+ strategy: name,
755
+ model: modelSpec,
756
+ variant,
757
+ schema: toJsonSchema(c.schema),
758
+ gold: c.gold
759
+ });
760
+ const cached = useCache ? await cacheGet(cacheDir, cacheKey) : void 0;
761
+ if (cached) {
762
+ cached.cached = true;
763
+ if (cached.costUsd === void 0)
764
+ cached.costUsd = estimateCostUsd(modelSpec, cached.usage);
765
+ cells.push(cached);
766
+ options.onCell?.({ caseId: c.id, track, strategy: name, cached: true });
767
+ continue;
768
+ }
769
+ options.onCell?.({ caseId: c.id, track, strategy: name, cached: false });
770
+ const artifacts = c.artifactsByTrack?.[track] ?? await materializeTrack(c.artifacts, track);
771
+ const start = Date.now();
772
+ let data;
773
+ let usage = emptyUsage();
774
+ let valid = true;
775
+ let error;
776
+ try {
777
+ const result = await extract({ artifacts, schema: c.schema, strategy });
778
+ data = result.data;
779
+ usage = result.usage;
780
+ if (result.error) {
781
+ valid = false;
782
+ error = result.error.message;
783
+ }
784
+ } catch (e) {
785
+ valid = false;
786
+ error = e.message;
787
+ data = null;
788
+ }
789
+ const cell = {
790
+ caseId: c.id,
791
+ track,
792
+ strategy: name,
793
+ model: modelSpec,
794
+ cached: false,
795
+ valid,
796
+ error,
797
+ score: scoreData(c.gold, c.transform ? c.transform(data) : data, c.metrics),
798
+ usage,
799
+ latencyMs: Date.now() - start,
800
+ costUsd: estimateCostUsd(modelSpec, usage),
801
+ data
802
+ };
803
+ if (useCache) await cacheSet(cacheDir, cacheKey, cell);
804
+ cells.push(cell);
805
+ }
806
+ }
807
+ }
808
+ return buildReport(modelSpec, cells, variant);
809
+ }
810
+ function buildReport(model, cells, variant) {
811
+ const summary = /* @__PURE__ */ new Map();
812
+ for (const cell of cells) {
813
+ const key = `${cell.strategy}|${cell.track}`;
814
+ let row = summary.get(key);
815
+ if (!row) {
816
+ row = {
817
+ strategy: cell.strategy,
818
+ track: cell.track,
819
+ cases: 0,
820
+ meanF1: 0,
821
+ meanPrecision: 0,
822
+ meanRecall: 0,
823
+ validityRate: 0,
824
+ exactMatchRate: 0,
825
+ totalInputTokens: 0,
826
+ totalOutputTokens: 0,
827
+ totalCostUsd: 0,
828
+ meanCostUsd: 0,
829
+ totalLatencyMs: 0,
830
+ meanLatencyMs: 0
831
+ };
832
+ summary.set(key, row);
833
+ }
834
+ row.cases++;
835
+ row.meanF1 += cell.score.f1;
836
+ row.meanPrecision += cell.score.precision;
837
+ row.meanRecall += cell.score.recall;
838
+ if (cell.valid) row.validityRate++;
839
+ if (cell.score.exactMatch) row.exactMatchRate++;
840
+ row.totalInputTokens += cell.usage.inputTokens;
841
+ row.totalOutputTokens += cell.usage.outputTokens;
842
+ row.totalCostUsd += cell.costUsd ?? estimateCostUsd(model, cell.usage);
843
+ row.totalLatencyMs += cell.latencyMs;
844
+ row.meanLatencyMs += cell.latencyMs;
845
+ }
846
+ for (const row of summary.values()) {
847
+ row.meanF1 /= row.cases;
848
+ row.meanPrecision /= row.cases;
849
+ row.meanRecall /= row.cases;
850
+ row.validityRate /= row.cases;
851
+ row.exactMatchRate /= row.cases;
852
+ row.meanCostUsd = row.cases ? row.totalCostUsd / row.cases : 0;
853
+ row.meanLatencyMs = row.cases ? row.meanLatencyMs / row.cases : 0;
854
+ }
855
+ return {
856
+ model,
857
+ variant,
858
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
859
+ cells,
860
+ summary: [...summary.values()]
861
+ };
862
+ }
863
+
864
+ // src/report.ts
865
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
866
+ import path3 from "path";
867
+ var pct = (v) => `${(v * 100).toFixed(1)}%`;
868
+ function toMarkdownTable(report) {
869
+ const lines = [];
870
+ lines.push(`# Benchmark report`);
871
+ lines.push(``);
872
+ lines.push(`- model: \`${report.model}\``);
873
+ if (report.variant) lines.push(`- variant: \`${report.variant}\``);
874
+ lines.push(`- generated: ${report.generatedAt}`);
875
+ lines.push(``);
876
+ lines.push(
877
+ `| strategy | track | cases | F1 | precision | recall | valid | exact | in tok | out tok | cost (USD) | ms/case | total ms |`
878
+ );
879
+ lines.push(`|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|`);
880
+ for (const row of report.summary) {
881
+ lines.push(
882
+ `| ${row.strategy} | ${row.track} | ${row.cases} | ${pct(row.meanF1)} | ${pct(row.meanPrecision)} | ${pct(row.meanRecall)} | ${pct(row.validityRate)} | ${pct(row.exactMatchRate)} | ${row.totalInputTokens} | ${row.totalOutputTokens} | ${row.totalCostUsd.toFixed(4)} | ${Math.round(row.meanLatencyMs)} | ${row.totalLatencyMs} |`
883
+ );
884
+ }
885
+ return lines.join("\n");
886
+ }
887
+ async function saveReport(report, file) {
888
+ await mkdir3(path3.dirname(file), { recursive: true });
889
+ await writeFile3(file, JSON.stringify(report, null, 2), "utf8");
890
+ const mdFile = file.replace(/\.json$/, "") + ".md";
891
+ await writeFile3(mdFile, toMarkdownTable(report), "utf8");
892
+ }
893
+
894
+ // src/cli.ts
895
+ var args = process.argv.slice(2);
896
+ var cmd = args[0];
897
+ async function main() {
898
+ switch (cmd) {
899
+ case "ls": {
900
+ const { readdir } = await import("fs/promises");
901
+ const { defaultDatasetDir: defaultDatasetDir2 } = await Promise.resolve().then(() => (init_config(), config_exports));
902
+ const dir = defaultDatasetDir2();
903
+ try {
904
+ const entries = await readdir(dir, { withFileTypes: true });
905
+ for (const e of entries) {
906
+ if (e.isDirectory()) console.log(` ${e.name}`);
907
+ }
908
+ } catch {
909
+ console.log(`Cache dir ${dir} is empty or missing.`);
910
+ }
911
+ break;
912
+ }
913
+ case "fetch": {
914
+ const all = args.includes("--all");
915
+ const name = args[1];
916
+ const datasets = { sroie };
917
+ if (all) {
918
+ for (const [key, ds] of Object.entries(datasets)) {
919
+ console.log(`Fetching ${key}...`);
920
+ await loadDataset(ds, { force: true });
921
+ console.log(` done.`);
922
+ }
923
+ } else if (name && name in datasets) {
924
+ console.log(`Fetching ${name}...`);
925
+ await loadDataset(datasets[name], { force: true });
926
+ console.log(` done.`);
927
+ } else {
928
+ console.log("Usage: struktur-benchmarks fetch <dataset> | --all");
929
+ console.log(`Available: ${Object.keys(datasets).join(", ")}`);
930
+ }
931
+ break;
932
+ }
933
+ case "run": {
934
+ const modulePath = args[1];
935
+ if (!modulePath) {
936
+ console.log("Usage: struktur-benchmarks run <module.js>");
937
+ console.log(" Module must export: { cases, strategies }");
938
+ process.exit(1);
939
+ }
940
+ const mod = await import(resolve(modulePath));
941
+ const report = await runBenchmark({
942
+ cases: mod.cases,
943
+ strategies: mod.strategies ?? ["simple"]
944
+ });
945
+ console.log(toMarkdownTable(report));
946
+ await saveReport(report, `benchmark-report-${Date.now()}.json`);
947
+ break;
948
+ }
949
+ default:
950
+ console.log("Usage: struktur-benchmarks <ls|fetch|run>");
951
+ console.log(" ls List cached datasets");
952
+ console.log(" fetch Download a dataset");
953
+ console.log(" run Run a module");
954
+ break;
955
+ }
956
+ }
957
+ main().catch((e) => {
958
+ console.error(e);
959
+ process.exit(1);
960
+ });
961
+ //# sourceMappingURL=cli.js.map