@sembl/testing 0.4.0 → 0.5.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/dist/index.cjs +16 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -54,7 +54,9 @@ function recordingKey(request) {
|
|
|
54
54
|
const material = stable({
|
|
55
55
|
systemPrompt: request.systemPrompt,
|
|
56
56
|
userInput: request.userInput,
|
|
57
|
-
jsonSchema: request.jsonSchema
|
|
57
|
+
jsonSchema: request.jsonSchema,
|
|
58
|
+
// A repair call shares everything above with the call it repairs.
|
|
59
|
+
...request.history && request.history.length > 0 ? { history: request.history } : {}
|
|
58
60
|
});
|
|
59
61
|
return (0, import_node_crypto.createHash)("sha256").update(material).digest("hex").slice(0, 24);
|
|
60
62
|
}
|
|
@@ -79,8 +81,10 @@ var RecordingProvider = class {
|
|
|
79
81
|
constructor(inner, dir) {
|
|
80
82
|
this.inner = inner;
|
|
81
83
|
this.dir = dir;
|
|
84
|
+
this.supportsHistory = inner.supportsHistory === true;
|
|
82
85
|
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
83
86
|
}
|
|
87
|
+
supportsHistory;
|
|
84
88
|
async complete(request) {
|
|
85
89
|
const response = await this.inner.complete(request);
|
|
86
90
|
const recording = {
|
|
@@ -89,7 +93,8 @@ var RecordingProvider = class {
|
|
|
89
93
|
request: {
|
|
90
94
|
systemPrompt: request.systemPrompt,
|
|
91
95
|
userInput: request.userInput,
|
|
92
|
-
jsonSchema: request.jsonSchema
|
|
96
|
+
jsonSchema: request.jsonSchema,
|
|
97
|
+
...request.history ? { history: request.history } : {}
|
|
93
98
|
},
|
|
94
99
|
response,
|
|
95
100
|
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -102,8 +107,15 @@ var ReplayProvider = class {
|
|
|
102
107
|
constructor(dir, options = {}) {
|
|
103
108
|
this.dir = dir;
|
|
104
109
|
this.recorder = options.fallback ? new RecordingProvider(options.fallback, dir) : void 0;
|
|
110
|
+
this.supportsHistory = options.fallback ? options.fallback.supportsHistory === true : true;
|
|
105
111
|
}
|
|
106
112
|
recorder;
|
|
113
|
+
/**
|
|
114
|
+
* Follows the fallback when there is one. A strict replay claims support
|
|
115
|
+
* so that a recorded multi-turn repair is asked for the same way it was
|
|
116
|
+
* recorded; the recordings decide whether it is answered.
|
|
117
|
+
*/
|
|
118
|
+
supportsHistory;
|
|
107
119
|
async complete(request) {
|
|
108
120
|
const path = recordingPath(this.dir, request);
|
|
109
121
|
if ((0, import_node_fs.existsSync)(path)) {
|
|
@@ -292,7 +304,9 @@ var context = new import_node_async_hooks.AsyncLocalStorage();
|
|
|
292
304
|
var MeteredProvider = class {
|
|
293
305
|
constructor(inner) {
|
|
294
306
|
this.inner = inner;
|
|
307
|
+
this.supportsHistory = inner.supportsHistory === true;
|
|
295
308
|
}
|
|
309
|
+
supportsHistory;
|
|
296
310
|
async complete(request) {
|
|
297
311
|
const response = await this.inner.complete(request);
|
|
298
312
|
const ctx = context.getStore();
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/replay.ts","../src/eval.ts"],"sourcesContent":["export {\n RecordingProvider,\n ReplayProvider,\n ReplayMissError,\n replayOrRecord,\n recordingKey,\n recordingPath,\n} from \"./replay.js\";\nexport type { Recording, ReplayOptions } from \"./replay.js\";\n\nexport {\n runEval,\n loadFixtures,\n compareLeaves,\n flattenLeaves,\n leavesEqual,\n fieldStats,\n estimateCost,\n saveReport,\n loadReport,\n diffReports,\n formatReport,\n} from \"./eval.js\";\nexport type {\n EvalFixture,\n EvalOptions,\n EvalReport,\n EvalItem,\n EvalTotals,\n EvalDiff,\n FieldDelta,\n FieldStats,\n LeafResult,\n LeafOutcome,\n TokenPrices,\n Usage,\n} from \"./eval.js\";\n","import { createHash } from \"node:crypto\";\nimport { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Provider, ProviderRequest, ProviderResponse } from \"@sembl/core\";\n\n/** What a recording holds: enough of the request to recognise it, and the answer. */\nexport interface Recording {\n /** Hash of the parts of the request that determine the answer. */\n key: string;\n schemaId: string;\n request: {\n systemPrompt: string;\n userInput: string;\n jsonSchema: Record<string, unknown>;\n };\n response: ProviderResponse;\n recordedAt: string;\n}\n\n/** JSON with sorted keys, so the same request always hashes the same. */\nfunction stable(value: unknown): string {\n return JSON.stringify(value, (_key, v) =>\n v && typeof v === \"object\" && !Array.isArray(v)\n ? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : v,\n );\n}\n\n/**\n * The key a request records under. Only the parts that reach the model\n * count: the system prompt, the user input and the JSON Schema. The runtime\n * schema and bundle are already folded into those.\n */\nexport function recordingKey(request: ProviderRequest): string {\n const material = stable({\n systemPrompt: request.systemPrompt,\n userInput: request.userInput,\n jsonSchema: request.jsonSchema,\n });\n return createHash(\"sha256\").update(material).digest(\"hex\").slice(0, 24);\n}\n\nfunction slug(id: string): string {\n return id.replace(/[^A-Za-z0-9_-]+/g, \"_\").slice(0, 40) || \"schema\";\n}\n\n/** Where a recording for a request lives inside a directory. */\nexport function recordingPath(dir: string, request: ProviderRequest): string {\n return join(dir, `${slug(request.schema.id)}.${recordingKey(request)}.json`);\n}\n\n/** Thrown by {@link ReplayProvider} when no recording matches a request. */\nexport class ReplayMissError extends Error {\n constructor(\n public readonly key: string,\n public readonly dir: string,\n public readonly schemaId: string,\n ) {\n super(\n `No recording for a \"${schemaId}\" request (key ${key}) in ${dir}. ` +\n \"Run once with a RecordingProvider, or pass a fallback provider to record misses.\",\n );\n this.name = \"ReplayMissError\";\n }\n}\n\n/**\n * Calls another provider and writes every request/response pair to a\n * directory, one JSON file each, named by schema and request hash. Run your\n * extraction code through it once with real credentials; the files it leaves\n * behind let a {@link ReplayProvider} answer the same requests offline.\n *\n * A request is identified by what reaches the model, so editing a field\n * description or the input produces a new recording rather than a stale hit.\n */\nexport class RecordingProvider implements Provider {\n constructor(\n private readonly inner: Provider,\n private readonly dir: string,\n ) {\n mkdirSync(dir, { recursive: true });\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const response = await this.inner.complete(request);\n const recording: Recording = {\n key: recordingKey(request),\n schemaId: request.schema.id,\n request: {\n systemPrompt: request.systemPrompt,\n userInput: request.userInput,\n jsonSchema: request.jsonSchema,\n },\n response,\n recordedAt: new Date().toISOString(),\n };\n writeFileSync(recordingPath(this.dir, request), JSON.stringify(recording, null, 2) + \"\\n\");\n return response;\n }\n}\n\n/** Options for {@link ReplayProvider}. */\nexport interface ReplayOptions {\n /**\n * Where to send a request no recording matches. The answer is recorded,\n * so the next run replays it. Without one, a miss throws\n * {@link ReplayMissError} — the right behaviour in CI, where a miss means\n * a fixture changed and nobody re-recorded.\n */\n fallback?: Provider;\n}\n\n/**\n * Answers requests from a directory of recordings, never touching the\n * network. Deterministic, free, and fast: the provider to put under tests of\n * your own extraction code.\n */\nexport class ReplayProvider implements Provider {\n private readonly recorder: RecordingProvider | undefined;\n\n constructor(\n private readonly dir: string,\n options: ReplayOptions = {},\n ) {\n this.recorder = options.fallback ? new RecordingProvider(options.fallback, dir) : undefined;\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const path = recordingPath(this.dir, request);\n if (existsSync(path)) {\n const recording = JSON.parse(readFileSync(path, \"utf8\")) as Recording;\n return recording.response;\n }\n if (this.recorder) {\n return this.recorder.complete(request);\n }\n throw new ReplayMissError(recordingKey(request), this.dir, request.schema.id);\n }\n\n /** How many recordings the directory holds. */\n size(): number {\n if (!existsSync(this.dir)) return 0;\n return readdirSync(this.dir).filter((f) => f.endsWith(\".json\")).length;\n }\n}\n\n/**\n * The usual arrangement: replay from `dir`, and record misses through\n * `live` when it is given — say, only when an API key is present — so the\n * same test file works locally with credentials and in CI without them.\n */\nexport function replayOrRecord(dir: string, live?: Provider): Provider {\n return new ReplayProvider(dir, live ? { fallback: live } : {});\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport {\n coerce,\n partialCoerce,\n coerceWithProvenance,\n partialCoerceWithProvenance,\n} from \"@sembl/core\";\nimport type {\n CoerceInput,\n CoerceOptions,\n FieldProvenance,\n Provider,\n ProviderRequest,\n ProviderResponse,\n ProviderUsage,\n ResolvedIssue,\n Source,\n} from \"@sembl/core\";\n\n/** One case: an input and what a correct extraction of it looks like. */\nexport interface EvalFixture {\n /** Shown in the report. Defaults to the file it was loaded from. */\n name?: string;\n input: CoerceInput;\n /** The expected extraction. Absent and `null` both mean \"not present\". */\n expected: Record<string, unknown>;\n}\n\n/** Per-million-token prices, for a cost line in the report. */\nexport interface TokenPrices {\n inputPerMTok: number;\n outputPerMTok: number;\n /** Defaults to `inputPerMTok` when the provider reports cache reads. */\n cacheReadPerMTok?: number;\n /** Defaults to `inputPerMTok` when the provider reports cache writes. */\n cacheWritePerMTok?: number;\n}\n\n/** Options for {@link runEval}. Everything in `CoerceOptions` applies per fixture. */\nexport interface EvalOptions extends CoerceOptions {\n fixtures: readonly EvalFixture[];\n /** Which coercion to run. Default `\"coerce\"`. */\n mode?: \"coerce\" | \"partialCoerce\";\n /** Ask for provenance, so the report can show confidence per field. */\n provenance?: boolean;\n /** How many fixtures run at once. Default 1, for stable latency numbers. */\n concurrency?: number;\n /** Prices for the cost line. Without them the report has no cost. */\n prices?: TokenPrices;\n}\n\n/** How one leaf compared. */\nexport type LeafOutcome = \"match\" | \"wrong\" | \"missing\" | \"extra\";\n\nexport interface LeafResult {\n path: string;\n outcome: LeafOutcome;\n expected?: unknown;\n actual?: unknown;\n confidence?: FieldProvenance[\"confidence\"];\n}\n\nexport interface EvalItem {\n name: string;\n ok: boolean;\n /** Message of the error the coercion threw, when it did. */\n error?: string;\n /** Whether every expected leaf matched and nothing extra came back. */\n exact: boolean;\n leaves: LeafResult[];\n issues: ResolvedIssue[];\n latencyMs: number;\n calls: number;\n usage: Usage;\n}\n\nexport interface Usage {\n promptTokens: number;\n completionTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n}\n\nexport interface FieldStats {\n path: string;\n tp: number;\n fp: number;\n fn: number;\n /** `tp / (tp + fp)`; null when nothing was returned for the field. */\n precision: number | null;\n /** `tp / (tp + fn)`; null when nothing was expected for the field. */\n recall: number | null;\n}\n\nexport interface EvalTotals {\n fixtures: number;\n ok: number;\n exact: number;\n precision: number | null;\n recall: number | null;\n usage: Usage;\n calls: number;\n cost?: number;\n latencyMs: { p50: number; p95: number; max: number; total: number };\n}\n\nexport interface EvalReport {\n schemaId: string;\n mode: \"coerce\" | \"partialCoerce\";\n ranAt: string;\n items: EvalItem[];\n fields: FieldStats[];\n totals: EvalTotals;\n}\n\n// ---------------------------------------------------------------------------\n// Fixtures\n\ninterface FileInput {\n file: string;\n label?: string;\n}\n\nfunction isFileInput(value: unknown): value is FileInput {\n return typeof value === \"object\" && value !== null && typeof (value as FileInput).file === \"string\";\n}\n\n/** Resolve `{ file, label }` inputs against the fixture's own directory. */\nfunction resolveInput(input: unknown, baseDir: string): CoerceInput {\n const one = (value: unknown): Source | string => {\n if (typeof value === \"string\") return value;\n if (isFileInput(value)) {\n const text = readFileSync(resolve(baseDir, value.file), \"utf8\");\n return value.label ? { label: value.label, text } : { text };\n }\n return value as Source;\n };\n if (Array.isArray(input)) {\n return input.map((v) => {\n const r = one(v);\n return typeof r === \"string\" ? { text: r } : r;\n });\n }\n return one(input);\n}\n\n/**\n * Load fixtures from a directory: every `*.json` file is either one fixture\n * or an array of them. An input may be `{ \"file\": \"page.html\", \"label\": … }`\n * to pull text from a sibling file, which keeps large scraped pages out of\n * the JSON.\n */\nexport function loadFixtures(dir: string): EvalFixture[] {\n const root = resolve(dir);\n if (!existsSync(root)) {\n throw new Error(`Fixture directory not found: ${root}`);\n }\n const fixtures: EvalFixture[] = [];\n const files = readdirSync(root)\n .filter((f) => f.endsWith(\".json\") && !f.startsWith(\".\"))\n .sort();\n for (const file of files) {\n const path = join(root, file);\n const parsed = JSON.parse(readFileSync(path, \"utf8\")) as unknown;\n const list = Array.isArray(parsed) ? parsed : [parsed];\n list.forEach((raw, index) => {\n const fixture = raw as EvalFixture;\n if (!fixture || typeof fixture !== \"object\" || !(\"input\" in fixture) || !(\"expected\" in fixture)) {\n throw new Error(`${path}${list.length > 1 ? `[${index}]` : \"\"}: a fixture needs \"input\" and \"expected\"`);\n }\n const stem = basename(file, \".json\");\n fixtures.push({\n name: fixture.name ?? (list.length > 1 ? `${stem}[${index}]` : stem),\n input: resolveInput(fixture.input, dirname(path)),\n expected: fixture.expected,\n });\n });\n }\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Scoring\n\nfunction stable(value: unknown): string {\n return JSON.stringify(value, (_k, v) =>\n v && typeof v === \"object\" && !Array.isArray(v)\n ? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : v,\n );\n}\n\nfunction isPrimitive(value: unknown): boolean {\n return value === null || typeof value !== \"object\";\n}\n\n/**\n * Flatten a value to its leaves, keyed by dotted path. Objects recurse;\n * arrays are leaves. `null` and `undefined` are absence, not leaves.\n */\nexport function flattenLeaves(value: unknown, prefix = \"\"): Map<string, unknown> {\n const leaves = new Map<string, unknown>();\n if (value === null || value === undefined) return leaves;\n if (typeof value !== \"object\" || Array.isArray(value)) {\n leaves.set(prefix, value);\n return leaves;\n }\n for (const [key, child] of Object.entries(value as Record<string, unknown>)) {\n const path = prefix ? `${prefix}.${key}` : key;\n for (const [leafPath, leaf] of flattenLeaves(child, path)) {\n leaves.set(leafPath, leaf);\n }\n }\n return leaves;\n}\n\n/**\n * Whether two leaves agree. Arrays of primitives compare as multisets —\n * the order amenities come back in is not a fact about the listing —\n * and everything else compares structurally.\n */\nexport function leavesEqual(a: unknown, b: unknown): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n if (a.every(isPrimitive) && b.every(isPrimitive)) {\n const sortedA = [...a].map(String).sort();\n const sortedB = [...b].map(String).sort();\n return sortedA.every((v, i) => v === sortedB[i]);\n }\n }\n return stable(a) === stable(b);\n}\n\n/** Compare one extraction against its expectation, leaf by leaf. */\nexport function compareLeaves(\n expected: Record<string, unknown>,\n actual: Record<string, unknown> | undefined,\n provenance: Record<string, FieldProvenance> = {},\n): LeafResult[] {\n const want = flattenLeaves(expected);\n const got = flattenLeaves(actual ?? {});\n const paths = [...new Set([...want.keys(), ...got.keys()])].sort();\n return paths.map((path) => {\n const top = path.split(\".\")[0];\n const confidence = provenance[top]?.confidence;\n const result: LeafResult = { path, outcome: \"match\" };\n if (confidence) result.confidence = confidence;\n if (want.has(path) && got.has(path)) {\n result.expected = want.get(path);\n result.actual = got.get(path);\n result.outcome = leavesEqual(want.get(path), got.get(path)) ? \"match\" : \"wrong\";\n } else if (want.has(path)) {\n result.expected = want.get(path);\n result.outcome = \"missing\";\n } else {\n result.actual = got.get(path);\n result.outcome = \"extra\";\n }\n return result;\n });\n}\n\nfunction ratio(num: number, den: number): number | null {\n return den === 0 ? null : num / den;\n}\n\n/** Aggregate leaf outcomes into per-field precision and recall. */\nexport function fieldStats(items: readonly EvalItem[]): FieldStats[] {\n const byPath = new Map<string, { tp: number; fp: number; fn: number }>();\n for (const item of items) {\n for (const leaf of item.leaves) {\n const stats = byPath.get(leaf.path) ?? { tp: 0, fp: 0, fn: 0 };\n switch (leaf.outcome) {\n case \"match\":\n stats.tp += 1;\n break;\n case \"wrong\":\n stats.fp += 1;\n stats.fn += 1;\n break;\n case \"missing\":\n stats.fn += 1;\n break;\n case \"extra\":\n stats.fp += 1;\n break;\n }\n byPath.set(leaf.path, stats);\n }\n }\n return [...byPath.entries()]\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([path, { tp, fp, fn }]) => ({\n path,\n tp,\n fp,\n fn,\n precision: ratio(tp, tp + fp),\n recall: ratio(tp, tp + fn),\n }));\n}\n\nfunction percentile(sorted: readonly number[], p: number): number {\n if (sorted.length === 0) return 0;\n const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);\n return sorted[Math.max(0, index)];\n}\n\nfunction emptyUsage(): Usage {\n return { promptTokens: 0, completionTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };\n}\n\nfunction addUsage(into: Usage, usage: ProviderUsage | undefined): void {\n if (!usage) return;\n into.promptTokens += usage.promptTokens;\n into.completionTokens += usage.completionTokens;\n into.cacheReadTokens += usage.cacheReadTokens ?? 0;\n into.cacheWriteTokens += usage.cacheWriteTokens ?? 0;\n}\n\n/** Dollars, from per-million-token prices. */\nexport function estimateCost(usage: Usage, prices: TokenPrices): number {\n const per = (tokens: number, price: number) => (tokens / 1_000_000) * price;\n return (\n per(usage.promptTokens, prices.inputPerMTok) +\n per(usage.completionTokens, prices.outputPerMTok) +\n per(usage.cacheReadTokens, prices.cacheReadPerMTok ?? prices.inputPerMTok) +\n per(usage.cacheWriteTokens, prices.cacheWritePerMTok ?? prices.inputPerMTok)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Running\n\ninterface ItemContext {\n usage: Usage;\n calls: number;\n}\n\nconst context = new AsyncLocalStorage<ItemContext>();\n\n/** Credits each call's usage to whichever fixture is running it. */\nclass MeteredProvider implements Provider {\n constructor(private readonly inner: Provider) {}\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const response = await this.inner.complete(request);\n const ctx = context.getStore();\n if (ctx) {\n ctx.calls += 1;\n addUsage(ctx.usage, response.usage);\n }\n return response;\n }\n}\n\nasync function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results: R[] = new Array(items.length);\n let next = 0;\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {\n while (next < items.length) {\n const index = next++;\n results[index] = await fn(items[index], index);\n }\n });\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Run every fixture through a coercion and score the results.\n *\n * Per-field precision and recall make a description change measurable: a\n * field whose recall dropped after a rewording is a field the rewording\n * hurt. Token usage and latency come along so the cost of a change is\n * visible next to its effect. Pair it with a `ReplayProvider` to run in CI\n * without spend.\n */\nexport async function runEval(options: EvalOptions): Promise<EvalReport> {\n const {\n fixtures,\n mode = \"coerce\",\n provenance = false,\n concurrency = 1,\n prices,\n provider,\n ...coerceOptions\n } = options;\n const metered = new MeteredProvider(provider);\n const run = mode === \"coerce\"\n ? provenance ? coerceWithProvenance : coerce\n : provenance ? partialCoerceWithProvenance : partialCoerce;\n\n const items = await mapWithConcurrency(fixtures, concurrency, async (fixture, index) => {\n const ctx: ItemContext = { usage: emptyUsage(), calls: 0 };\n const name = fixture.name ?? `fixture ${index + 1}`;\n const started = performance.now();\n return context.run(ctx, async (): Promise<EvalItem> => {\n try {\n const result = await run<Record<string, unknown>>(fixture.input, {\n ...coerceOptions,\n provider: metered,\n });\n const data = provenance\n ? (result as { data: Record<string, unknown> }).data\n : (result as Record<string, unknown>);\n const prov = provenance ? (result as { provenance: Record<string, FieldProvenance> }).provenance : {};\n const issues = provenance ? (result as { issues: ResolvedIssue[] }).issues : [];\n const leaves = compareLeaves(fixture.expected, data, prov);\n return {\n name,\n ok: true,\n exact: leaves.every((l) => l.outcome === \"match\"),\n leaves,\n issues,\n latencyMs: Math.round(performance.now() - started),\n calls: ctx.calls,\n usage: ctx.usage,\n };\n } catch (error) {\n return {\n name,\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n exact: false,\n leaves: compareLeaves(fixture.expected, undefined),\n issues: [],\n latencyMs: Math.round(performance.now() - started),\n calls: ctx.calls,\n usage: ctx.usage,\n };\n }\n });\n });\n\n const fields = fieldStats(items);\n const tp = fields.reduce((s, f) => s + f.tp, 0);\n const fp = fields.reduce((s, f) => s + f.fp, 0);\n const fn = fields.reduce((s, f) => s + f.fn, 0);\n const usage = emptyUsage();\n for (const item of items) {\n usage.promptTokens += item.usage.promptTokens;\n usage.completionTokens += item.usage.completionTokens;\n usage.cacheReadTokens += item.usage.cacheReadTokens;\n usage.cacheWriteTokens += item.usage.cacheWriteTokens;\n }\n const latencies = items.map((i) => i.latencyMs).sort((a, b) => a - b);\n\n return {\n schemaId: options.schema.id,\n mode,\n ranAt: new Date().toISOString(),\n items,\n fields,\n totals: {\n fixtures: items.length,\n ok: items.filter((i) => i.ok).length,\n exact: items.filter((i) => i.exact).length,\n precision: ratio(tp, tp + fp),\n recall: ratio(tp, tp + fn),\n usage,\n calls: items.reduce((s, i) => s + i.calls, 0),\n ...(prices ? { cost: estimateCost(usage, prices) } : {}),\n latencyMs: {\n p50: percentile(latencies, 50),\n p95: percentile(latencies, 95),\n max: latencies[latencies.length - 1] ?? 0,\n total: latencies.reduce((s, l) => s + l, 0),\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Persistence and diffs\n\nexport function saveReport(report: EvalReport, file: string): void {\n mkdirSync(dirname(resolve(file)), { recursive: true });\n writeFileSync(resolve(file), JSON.stringify(report, null, 2) + \"\\n\");\n}\n\nexport function loadReport(file: string): EvalReport | undefined {\n const path = resolve(file);\n if (!existsSync(path)) return undefined;\n return JSON.parse(readFileSync(path, \"utf8\")) as EvalReport;\n}\n\nexport interface FieldDelta {\n path: string;\n precision: number | null;\n recall: number | null;\n}\n\n/** What changed between two runs. Deltas are `next − previous`. */\nexport interface EvalDiff {\n exact: number;\n ok: number;\n precision: number | null;\n recall: number | null;\n promptTokens: number;\n completionTokens: number;\n cost?: number;\n p50Ms: number;\n /** Fields whose precision or recall moved, largest drop first. */\n fields: FieldDelta[];\n}\n\nfunction delta(a: number | null | undefined, b: number | null | undefined): number | null {\n return a === null || a === undefined || b === null || b === undefined ? null : b - a;\n}\n\nexport function diffReports(previous: EvalReport, next: EvalReport): EvalDiff {\n const prevFields = new Map(previous.fields.map((f) => [f.path, f]));\n const fields: FieldDelta[] = [];\n for (const field of next.fields) {\n const before = prevFields.get(field.path);\n const precision = delta(before?.precision, field.precision);\n const recall = delta(before?.recall, field.recall);\n if ((precision !== null && precision !== 0) || (recall !== null && recall !== 0)) {\n fields.push({ path: field.path, precision, recall });\n }\n }\n fields.sort((a, b) => Math.min(a.precision ?? 0, a.recall ?? 0) - Math.min(b.precision ?? 0, b.recall ?? 0));\n\n return {\n exact: next.totals.exact - previous.totals.exact,\n ok: next.totals.ok - previous.totals.ok,\n precision: delta(previous.totals.precision, next.totals.precision),\n recall: delta(previous.totals.recall, next.totals.recall),\n promptTokens: next.totals.usage.promptTokens - previous.totals.usage.promptTokens,\n completionTokens: next.totals.usage.completionTokens - previous.totals.usage.completionTokens,\n ...(next.totals.cost !== undefined && previous.totals.cost !== undefined\n ? { cost: next.totals.cost - previous.totals.cost }\n : {}),\n p50Ms: next.totals.latencyMs.p50 - previous.totals.latencyMs.p50,\n fields,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Formatting\n\nfunction pct(value: number | null): string {\n return value === null ? \" – \" : `${(value * 100).toFixed(0).padStart(3)}%`;\n}\n\nfunction signed(value: number | null, digits = 0, suffix = \"\"): string {\n if (value === null || value === 0) return \"\";\n const text = digits > 0 ? value.toFixed(digits) : String(value);\n return ` (${value > 0 ? \"+\" : \"\"}${text}${suffix})`;\n}\n\nfunction signedPct(value: number | null): string {\n return value === null || value === 0 ? \"\" : ` (${value > 0 ? \"+\" : \"\"}${(value * 100).toFixed(0)}pt)`;\n}\n\n/** A plain-text report for a terminal, with deltas when a previous run is given. */\nexport function formatReport(report: EvalReport, diff?: EvalDiff): string {\n const t = report.totals;\n const lines: string[] = [];\n lines.push(\n `Eval: ${report.schemaId} (${report.mode}) — ${t.fixtures} fixture(s), ${t.ok} ran${signed(diff?.ok ?? null)}, ${t.exact} exact${signed(diff?.exact ?? null)}`,\n );\n lines.push(\"\");\n\n const width = Math.max(5, ...report.fields.map((f) => f.path.length));\n const deltas = new Map((diff?.fields ?? []).map((f) => [f.path, f]));\n lines.push(`${\"Field\".padEnd(width)} Prec Recall tp fp fn Δ`);\n const col = (value: number | null, w: number) => pct(value).trim().padStart(w);\n for (const field of report.fields) {\n const d = deltas.get(field.path);\n const change = d\n ? [d.precision !== null && d.precision !== 0 ? `P${signedPct(d.precision).trim()}` : \"\", d.recall !== null && d.recall !== 0 ? `R${signedPct(d.recall).trim()}` : \"\"]\n .filter(Boolean)\n .join(\" \")\n : \"\";\n lines.push(\n `${field.path.padEnd(width)} ${col(field.precision, 4)} ${col(field.recall, 6)} ${String(field.tp).padStart(2)} ${String(field.fp).padStart(2)} ${String(field.fn).padStart(2)} ${change}`,\n );\n }\n lines.push(\"\");\n lines.push(\n `Overall: precision ${pct(t.precision).trim()}${signedPct(diff?.precision ?? null)}, recall ${pct(t.recall).trim()}${signedPct(diff?.recall ?? null)}`,\n );\n const u = t.usage;\n const cache = u.cacheReadTokens || u.cacheWriteTokens\n ? `, cache read ${u.cacheReadTokens.toLocaleString(\"en-US\")} / write ${u.cacheWriteTokens.toLocaleString(\"en-US\")}`\n : \"\";\n lines.push(\n `Tokens: ${u.promptTokens.toLocaleString(\"en-US\")} prompt${signed(diff?.promptTokens ?? null)} / ${u.completionTokens.toLocaleString(\"en-US\")} completion${signed(diff?.completionTokens ?? null)}${cache}; ${t.calls} call(s)` +\n (t.cost !== undefined ? `; cost $${t.cost.toFixed(4)}${signed(diff?.cost ?? null, 4)}` : \"\"),\n );\n lines.push(\n `Latency: p50 ${t.latencyMs.p50}ms${signed(diff?.p50Ms ?? null, 0, \"ms\")}, p95 ${t.latencyMs.p95}ms, max ${t.latencyMs.max}ms`,\n );\n\n const failed = report.items.filter((i) => !i.ok);\n if (failed.length > 0) {\n lines.push(\"\");\n lines.push(\"Failed:\");\n for (const item of failed) lines.push(` ${item.name}: ${item.error}`);\n }\n const imperfect = report.items.filter((i) => i.ok && !i.exact);\n if (imperfect.length > 0) {\n lines.push(\"\");\n lines.push(\"Mismatches:\");\n for (const item of imperfect) {\n for (const leaf of item.leaves.filter((l) => l.outcome !== \"match\")) {\n const detail =\n leaf.outcome === \"wrong\"\n ? `expected ${JSON.stringify(leaf.expected)}, got ${JSON.stringify(leaf.actual)}`\n : leaf.outcome === \"missing\"\n ? `expected ${JSON.stringify(leaf.expected)}, got nothing`\n : `unexpected ${JSON.stringify(leaf.actual)}`;\n const conf = leaf.confidence ? ` [${leaf.confidence}]` : \"\";\n lines.push(` ${item.name} › ${leaf.path}: ${detail}${conf}`);\n }\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,yBAA2B;AAC3B,qBAAgF;AAChF,uBAAqB;AAkBrB,SAAS,OAAO,OAAwB;AACtC,SAAO,KAAK;AAAA,IAAU;AAAA,IAAO,CAAC,MAAM,MAClC,KAAK,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC,IAC1C,OAAO,YAAY,OAAO,QAAQ,CAA4B,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAE,CAAC,IAChH;AAAA,EACN;AACF;AAOO,SAAS,aAAa,SAAkC;AAC7D,QAAM,WAAW,OAAO;AAAA,IACtB,cAAc,QAAQ;AAAA,IACtB,WAAW,QAAQ;AAAA,IACnB,YAAY,QAAQ;AAAA,EACtB,CAAC;AACD,aAAO,+BAAW,QAAQ,EAAE,OAAO,QAAQ,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AACxE;AAEA,SAAS,KAAK,IAAoB;AAChC,SAAO,GAAG,QAAQ,oBAAoB,GAAG,EAAE,MAAM,GAAG,EAAE,KAAK;AAC7D;AAGO,SAAS,cAAc,KAAa,SAAkC;AAC3E,aAAO,uBAAK,KAAK,GAAG,KAAK,QAAQ,OAAO,EAAE,CAAC,IAAI,aAAa,OAAO,CAAC,OAAO;AAC7E;AAGO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EACzC,YACkB,KACA,KACA,UAChB;AACA;AAAA,MACE,uBAAuB,QAAQ,kBAAkB,GAAG,QAAQ,GAAG;AAAA,IAEjE;AAPgB;AACA;AACA;AAMhB,SAAK,OAAO;AAAA,EACd;AACF;AAWO,IAAM,oBAAN,MAA4C;AAAA,EACjD,YACmB,OACA,KACjB;AAFiB;AACA;AAEjB,kCAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AAAA,EAEA,MAAM,SAAS,SAAqD;AAClE,UAAM,WAAW,MAAM,KAAK,MAAM,SAAS,OAAO;AAClD,UAAM,YAAuB;AAAA,MAC3B,KAAK,aAAa,OAAO;AAAA,MACzB,UAAU,QAAQ,OAAO;AAAA,MACzB,SAAS;AAAA,QACP,cAAc,QAAQ;AAAA,QACtB,WAAW,QAAQ;AAAA,QACnB,YAAY,QAAQ;AAAA,MACtB;AAAA,MACA;AAAA,MACA,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IACrC;AACA,sCAAc,cAAc,KAAK,KAAK,OAAO,GAAG,KAAK,UAAU,WAAW,MAAM,CAAC,IAAI,IAAI;AACzF,WAAO;AAAA,EACT;AACF;AAkBO,IAAM,iBAAN,MAAyC;AAAA,EAG9C,YACmB,KACjB,UAAyB,CAAC,GAC1B;AAFiB;AAGjB,SAAK,WAAW,QAAQ,WAAW,IAAI,kBAAkB,QAAQ,UAAU,GAAG,IAAI;AAAA,EACpF;AAAA,EAPiB;AAAA,EASjB,MAAM,SAAS,SAAqD;AAClE,UAAM,OAAO,cAAc,KAAK,KAAK,OAAO;AAC5C,YAAI,2BAAW,IAAI,GAAG;AACpB,YAAM,YAAY,KAAK,UAAM,6BAAa,MAAM,MAAM,CAAC;AACvD,aAAO,UAAU;AAAA,IACnB;AACA,QAAI,KAAK,UAAU;AACjB,aAAO,KAAK,SAAS,SAAS,OAAO;AAAA,IACvC;AACA,UAAM,IAAI,gBAAgB,aAAa,OAAO,GAAG,KAAK,KAAK,QAAQ,OAAO,EAAE;AAAA,EAC9E;AAAA;AAAA,EAGA,OAAe;AACb,QAAI,KAAC,2BAAW,KAAK,GAAG,EAAG,QAAO;AAClC,eAAO,4BAAY,KAAK,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC,EAAE;AAAA,EAClE;AACF;AAOO,SAAS,eAAe,KAAa,MAA2B;AACrE,SAAO,IAAI,eAAe,KAAK,OAAO,EAAE,UAAU,KAAK,IAAI,CAAC,CAAC;AAC/D;;;ACzJA,8BAAkC;AAClC,IAAAA,kBAAgF;AAChF,IAAAC,oBAAiD;AACjD,kBAKO;AAqHP,SAAS,YAAY,OAAoC;AACvD,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAQ,MAAoB,SAAS;AAC7F;AAGA,SAAS,aAAa,OAAgB,SAA8B;AAClE,QAAM,MAAM,CAAC,UAAoC;AAC/C,QAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAI,YAAY,KAAK,GAAG;AACtB,YAAM,WAAO,kCAAa,2BAAQ,SAAS,MAAM,IAAI,GAAG,MAAM;AAC9D,aAAO,MAAM,QAAQ,EAAE,OAAO,MAAM,OAAO,KAAK,IAAI,EAAE,KAAK;AAAA,IAC7D;AACA,WAAO;AAAA,EACT;AACA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,CAAC,MAAM;AACtB,YAAM,IAAI,IAAI,CAAC;AACf,aAAO,OAAO,MAAM,WAAW,EAAE,MAAM,EAAE,IAAI;AAAA,IAC/C,CAAC;AAAA,EACH;AACA,SAAO,IAAI,KAAK;AAClB;AAQO,SAAS,aAAa,KAA4B;AACvD,QAAM,WAAO,2BAAQ,GAAG;AACxB,MAAI,KAAC,4BAAW,IAAI,GAAG;AACrB,UAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAAA,EACxD;AACA,QAAM,WAA0B,CAAC;AACjC,QAAM,YAAQ,6BAAY,IAAI,EAC3B,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,KAAK,CAAC,EAAE,WAAW,GAAG,CAAC,EACvD,KAAK;AACR,aAAW,QAAQ,OAAO;AACxB,UAAM,WAAO,wBAAK,MAAM,IAAI;AAC5B,UAAM,SAAS,KAAK,UAAM,8BAAa,MAAM,MAAM,CAAC;AACpD,UAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;AACrD,SAAK,QAAQ,CAAC,KAAK,UAAU;AAC3B,YAAM,UAAU;AAChB,UAAI,CAAC,WAAW,OAAO,YAAY,YAAY,EAAE,WAAW,YAAY,EAAE,cAAc,UAAU;AAChG,cAAM,IAAI,MAAM,GAAG,IAAI,GAAG,KAAK,SAAS,IAAI,IAAI,KAAK,MAAM,EAAE,0CAA0C;AAAA,MACzG;AACA,YAAM,WAAO,4BAAS,MAAM,OAAO;AACnC,eAAS,KAAK;AAAA,QACZ,MAAM,QAAQ,SAAS,KAAK,SAAS,IAAI,GAAG,IAAI,IAAI,KAAK,MAAM;AAAA,QAC/D,OAAO,aAAa,QAAQ,WAAO,2BAAQ,IAAI,CAAC;AAAA,QAChD,UAAU,QAAQ;AAAA,MACpB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAKA,SAASC,QAAO,OAAwB;AACtC,SAAO,KAAK;AAAA,IAAU;AAAA,IAAO,CAAC,IAAI,MAChC,KAAK,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC,IAC1C,OAAO,YAAY,OAAO,QAAQ,CAA4B,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAE,CAAC,IAChH;AAAA,EACN;AACF;AAEA,SAAS,YAAY,OAAyB;AAC5C,SAAO,UAAU,QAAQ,OAAO,UAAU;AAC5C;AAMO,SAAS,cAAc,OAAgB,SAAS,IAA0B;AAC/E,QAAM,SAAS,oBAAI,IAAqB;AACxC,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AACrD,WAAO,IAAI,QAAQ,KAAK;AACxB,WAAO;AAAA,EACT;AACA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAgC,GAAG;AAC3E,UAAM,OAAO,SAAS,GAAG,MAAM,IAAI,GAAG,KAAK;AAC3C,eAAW,CAAC,UAAU,IAAI,KAAK,cAAc,OAAO,IAAI,GAAG;AACzD,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,YAAY,GAAY,GAAqB;AAC3D,MAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;AACxC,QAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,QAAI,EAAE,MAAM,WAAW,KAAK,EAAE,MAAM,WAAW,GAAG;AAChD,YAAM,UAAU,CAAC,GAAG,CAAC,EAAE,IAAI,MAAM,EAAE,KAAK;AACxC,YAAM,UAAU,CAAC,GAAG,CAAC,EAAE,IAAI,MAAM,EAAE,KAAK;AACxC,aAAO,QAAQ,MAAM,CAAC,GAAG,MAAM,MAAM,QAAQ,CAAC,CAAC;AAAA,IACjD;AAAA,EACF;AACA,SAAOA,QAAO,CAAC,MAAMA,QAAO,CAAC;AAC/B;AAGO,SAAS,cACd,UACA,QACA,aAA8C,CAAC,GACjC;AACd,QAAM,OAAO,cAAc,QAAQ;AACnC,QAAM,MAAM,cAAc,UAAU,CAAC,CAAC;AACtC,QAAM,QAAQ,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,KAAK,KAAK,GAAG,GAAG,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK;AACjE,SAAO,MAAM,IAAI,CAAC,SAAS;AACzB,UAAM,MAAM,KAAK,MAAM,GAAG,EAAE,CAAC;AAC7B,UAAM,aAAa,WAAW,GAAG,GAAG;AACpC,UAAM,SAAqB,EAAE,MAAM,SAAS,QAAQ;AACpD,QAAI,WAAY,QAAO,aAAa;AACpC,QAAI,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,GAAG;AACnC,aAAO,WAAW,KAAK,IAAI,IAAI;AAC/B,aAAO,SAAS,IAAI,IAAI,IAAI;AAC5B,aAAO,UAAU,YAAY,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,CAAC,IAAI,UAAU;AAAA,IAC1E,WAAW,KAAK,IAAI,IAAI,GAAG;AACzB,aAAO,WAAW,KAAK,IAAI,IAAI;AAC/B,aAAO,UAAU;AAAA,IACnB,OAAO;AACL,aAAO,SAAS,IAAI,IAAI,IAAI;AAC5B,aAAO,UAAU;AAAA,IACnB;AACA,WAAO;AAAA,EACT,CAAC;AACH;AAEA,SAAS,MAAM,KAAa,KAA4B;AACtD,SAAO,QAAQ,IAAI,OAAO,MAAM;AAClC;AAGO,SAAS,WAAW,OAA0C;AACnE,QAAM,SAAS,oBAAI,IAAoD;AACvE,aAAW,QAAQ,OAAO;AACxB,eAAW,QAAQ,KAAK,QAAQ;AAC9B,YAAM,QAAQ,OAAO,IAAI,KAAK,IAAI,KAAK,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE;AAC7D,cAAQ,KAAK,SAAS;AAAA,QACpB,KAAK;AACH,gBAAM,MAAM;AACZ;AAAA,QACF,KAAK;AACH,gBAAM,MAAM;AACZ,gBAAM,MAAM;AACZ;AAAA,QACF,KAAK;AACH,gBAAM,MAAM;AACZ;AAAA,QACF,KAAK;AACH,gBAAM,MAAM;AACZ;AAAA,MACJ;AACA,aAAO,IAAI,KAAK,MAAM,KAAK;AAAA,IAC7B;AAAA,EACF;AACA,SAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,EACxB,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAE,EAC/C,IAAI,CAAC,CAAC,MAAM,EAAE,IAAI,IAAI,GAAG,CAAC,OAAO;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,MAAM,IAAI,KAAK,EAAE;AAAA,IAC5B,QAAQ,MAAM,IAAI,KAAK,EAAE;AAAA,EAC3B,EAAE;AACN;AAEA,SAAS,WAAW,QAA2B,GAAmB;AAChE,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,QAAQ,KAAK,IAAI,OAAO,SAAS,GAAG,KAAK,KAAM,IAAI,MAAO,OAAO,MAAM,IAAI,CAAC;AAClF,SAAO,OAAO,KAAK,IAAI,GAAG,KAAK,CAAC;AAClC;AAEA,SAAS,aAAoB;AAC3B,SAAO,EAAE,cAAc,GAAG,kBAAkB,GAAG,iBAAiB,GAAG,kBAAkB,EAAE;AACzF;AAEA,SAAS,SAAS,MAAa,OAAwC;AACrE,MAAI,CAAC,MAAO;AACZ,OAAK,gBAAgB,MAAM;AAC3B,OAAK,oBAAoB,MAAM;AAC/B,OAAK,mBAAmB,MAAM,mBAAmB;AACjD,OAAK,oBAAoB,MAAM,oBAAoB;AACrD;AAGO,SAAS,aAAa,OAAc,QAA6B;AACtE,QAAM,MAAM,CAAC,QAAgB,UAAmB,SAAS,MAAa;AACtE,SACE,IAAI,MAAM,cAAc,OAAO,YAAY,IAC3C,IAAI,MAAM,kBAAkB,OAAO,aAAa,IAChD,IAAI,MAAM,iBAAiB,OAAO,oBAAoB,OAAO,YAAY,IACzE,IAAI,MAAM,kBAAkB,OAAO,qBAAqB,OAAO,YAAY;AAE/E;AAUA,IAAM,UAAU,IAAI,0CAA+B;AAGnD,IAAM,kBAAN,MAA0C;AAAA,EACxC,YAA6B,OAAiB;AAAjB;AAAA,EAAkB;AAAA,EAE/C,MAAM,SAAS,SAAqD;AAClE,UAAM,WAAW,MAAM,KAAK,MAAM,SAAS,OAAO;AAClD,UAAM,MAAM,QAAQ,SAAS;AAC7B,QAAI,KAAK;AACP,UAAI,SAAS;AACb,eAAS,IAAI,OAAO,SAAS,KAAK;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AACF;AAEA,eAAe,mBACb,OACA,aACA,IACc;AACd,QAAM,UAAe,IAAI,MAAM,MAAM,MAAM;AAC3C,MAAI,OAAO;AACX,QAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,aAAa,MAAM,MAAM,EAAE,GAAG,YAAY;AACtF,WAAO,OAAO,MAAM,QAAQ;AAC1B,YAAM,QAAQ;AACd,cAAQ,KAAK,IAAI,MAAM,GAAG,MAAM,KAAK,GAAG,KAAK;AAAA,IAC/C;AAAA,EACF,CAAC;AACD,QAAM,QAAQ,IAAI,OAAO;AACzB,SAAO;AACT;AAWA,eAAsB,QAAQ,SAA2C;AACvE,QAAM;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,IACP,aAAa;AAAA,IACb,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AACJ,QAAM,UAAU,IAAI,gBAAgB,QAAQ;AAC5C,QAAM,MAAM,SAAS,WACjB,aAAa,mCAAuB,qBACpC,aAAa,0CAA8B;AAE/C,QAAM,QAAQ,MAAM,mBAAmB,UAAU,aAAa,OAAO,SAAS,UAAU;AACtF,UAAM,MAAmB,EAAE,OAAO,WAAW,GAAG,OAAO,EAAE;AACzD,UAAM,OAAO,QAAQ,QAAQ,WAAW,QAAQ,CAAC;AACjD,UAAM,UAAU,YAAY,IAAI;AAChC,WAAO,QAAQ,IAAI,KAAK,YAA+B;AACrD,UAAI;AACF,cAAM,SAAS,MAAM,IAA6B,QAAQ,OAAO;AAAA,UAC/D,GAAG;AAAA,UACH,UAAU;AAAA,QACZ,CAAC;AACD,cAAM,OAAO,aACR,OAA6C,OAC7C;AACL,cAAM,OAAO,aAAc,OAA2D,aAAa,CAAC;AACpG,cAAM,SAAS,aAAc,OAAuC,SAAS,CAAC;AAC9E,cAAM,SAAS,cAAc,QAAQ,UAAU,MAAM,IAAI;AACzD,eAAO;AAAA,UACL;AAAA,UACA,IAAI;AAAA,UACJ,OAAO,OAAO,MAAM,CAAC,MAAM,EAAE,YAAY,OAAO;AAAA,UAChD;AAAA,UACA;AAAA,UACA,WAAW,KAAK,MAAM,YAAY,IAAI,IAAI,OAAO;AAAA,UACjD,OAAO,IAAI;AAAA,UACX,OAAO,IAAI;AAAA,QACb;AAAA,MACF,SAAS,OAAO;AACd,eAAO;AAAA,UACL;AAAA,UACA,IAAI;AAAA,UACJ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,UAC5D,OAAO;AAAA,UACP,QAAQ,cAAc,QAAQ,UAAU,MAAS;AAAA,UACjD,QAAQ,CAAC;AAAA,UACT,WAAW,KAAK,MAAM,YAAY,IAAI,IAAI,OAAO;AAAA,UACjD,OAAO,IAAI;AAAA,UACX,OAAO,IAAI;AAAA,QACb;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAED,QAAM,SAAS,WAAW,KAAK;AAC/B,QAAM,KAAK,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,IAAI,CAAC;AAC9C,QAAM,KAAK,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,IAAI,CAAC;AAC9C,QAAM,KAAK,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,IAAI,CAAC;AAC9C,QAAM,QAAQ,WAAW;AACzB,aAAW,QAAQ,OAAO;AACxB,UAAM,gBAAgB,KAAK,MAAM;AACjC,UAAM,oBAAoB,KAAK,MAAM;AACrC,UAAM,mBAAmB,KAAK,MAAM;AACpC,UAAM,oBAAoB,KAAK,MAAM;AAAA,EACvC;AACA,QAAM,YAAY,MAAM,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAEpE,SAAO;AAAA,IACL,UAAU,QAAQ,OAAO;AAAA,IACzB;AAAA,IACA,QAAO,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC9B;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,MACN,UAAU,MAAM;AAAA,MAChB,IAAI,MAAM,OAAO,CAAC,MAAM,EAAE,EAAE,EAAE;AAAA,MAC9B,OAAO,MAAM,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE;AAAA,MACpC,WAAW,MAAM,IAAI,KAAK,EAAE;AAAA,MAC5B,QAAQ,MAAM,IAAI,KAAK,EAAE;AAAA,MACzB;AAAA,MACA,OAAO,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,OAAO,CAAC;AAAA,MAC5C,GAAI,SAAS,EAAE,MAAM,aAAa,OAAO,MAAM,EAAE,IAAI,CAAC;AAAA,MACtD,WAAW;AAAA,QACT,KAAK,WAAW,WAAW,EAAE;AAAA,QAC7B,KAAK,WAAW,WAAW,EAAE;AAAA,QAC7B,KAAK,UAAU,UAAU,SAAS,CAAC,KAAK;AAAA,QACxC,OAAO,UAAU,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AACF;AAKO,SAAS,WAAW,QAAoB,MAAoB;AACjE,qCAAU,+BAAQ,2BAAQ,IAAI,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACrD,yCAAc,2BAAQ,IAAI,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,IAAI;AACrE;AAEO,SAAS,WAAW,MAAsC;AAC/D,QAAM,WAAO,2BAAQ,IAAI;AACzB,MAAI,KAAC,4BAAW,IAAI,EAAG,QAAO;AAC9B,SAAO,KAAK,UAAM,8BAAa,MAAM,MAAM,CAAC;AAC9C;AAsBA,SAAS,MAAM,GAA8B,GAA6C;AACxF,SAAO,MAAM,QAAQ,MAAM,UAAa,MAAM,QAAQ,MAAM,SAAY,OAAO,IAAI;AACrF;AAEO,SAAS,YAAY,UAAsB,MAA4B;AAC5E,QAAM,aAAa,IAAI,IAAI,SAAS,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAClE,QAAM,SAAuB,CAAC;AAC9B,aAAW,SAAS,KAAK,QAAQ;AAC/B,UAAM,SAAS,WAAW,IAAI,MAAM,IAAI;AACxC,UAAM,YAAY,MAAM,QAAQ,WAAW,MAAM,SAAS;AAC1D,UAAM,SAAS,MAAM,QAAQ,QAAQ,MAAM,MAAM;AACjD,QAAK,cAAc,QAAQ,cAAc,KAAO,WAAW,QAAQ,WAAW,GAAI;AAChF,aAAO,KAAK,EAAE,MAAM,MAAM,MAAM,WAAW,OAAO,CAAC;AAAA,IACrD;AAAA,EACF;AACA,SAAO,KAAK,CAAC,GAAG,MAAM,KAAK,IAAI,EAAE,aAAa,GAAG,EAAE,UAAU,CAAC,IAAI,KAAK,IAAI,EAAE,aAAa,GAAG,EAAE,UAAU,CAAC,CAAC;AAE3G,SAAO;AAAA,IACL,OAAO,KAAK,OAAO,QAAQ,SAAS,OAAO;AAAA,IAC3C,IAAI,KAAK,OAAO,KAAK,SAAS,OAAO;AAAA,IACrC,WAAW,MAAM,SAAS,OAAO,WAAW,KAAK,OAAO,SAAS;AAAA,IACjE,QAAQ,MAAM,SAAS,OAAO,QAAQ,KAAK,OAAO,MAAM;AAAA,IACxD,cAAc,KAAK,OAAO,MAAM,eAAe,SAAS,OAAO,MAAM;AAAA,IACrE,kBAAkB,KAAK,OAAO,MAAM,mBAAmB,SAAS,OAAO,MAAM;AAAA,IAC7E,GAAI,KAAK,OAAO,SAAS,UAAa,SAAS,OAAO,SAAS,SAC3D,EAAE,MAAM,KAAK,OAAO,OAAO,SAAS,OAAO,KAAK,IAChD,CAAC;AAAA,IACL,OAAO,KAAK,OAAO,UAAU,MAAM,SAAS,OAAO,UAAU;AAAA,IAC7D;AAAA,EACF;AACF;AAKA,SAAS,IAAI,OAA8B;AACzC,SAAO,UAAU,OAAO,eAAU,IAAI,QAAQ,KAAK,QAAQ,CAAC,EAAE,SAAS,CAAC,CAAC;AAC3E;AAEA,SAAS,OAAO,OAAsB,SAAS,GAAG,SAAS,IAAY;AACrE,MAAI,UAAU,QAAQ,UAAU,EAAG,QAAO;AAC1C,QAAM,OAAO,SAAS,IAAI,MAAM,QAAQ,MAAM,IAAI,OAAO,KAAK;AAC9D,SAAO,KAAK,QAAQ,IAAI,MAAM,EAAE,GAAG,IAAI,GAAG,MAAM;AAClD;AAEA,SAAS,UAAU,OAA8B;AAC/C,SAAO,UAAU,QAAQ,UAAU,IAAI,KAAK,KAAK,QAAQ,IAAI,MAAM,EAAE,IAAI,QAAQ,KAAK,QAAQ,CAAC,CAAC;AAClG;AAGO,SAAS,aAAa,QAAoB,MAAyB;AACxE,QAAM,IAAI,OAAO;AACjB,QAAM,QAAkB,CAAC;AACzB,QAAM;AAAA,IACJ,SAAS,OAAO,QAAQ,KAAK,OAAO,IAAI,YAAO,EAAE,QAAQ,gBAAgB,EAAE,EAAE,OAAO,OAAO,MAAM,MAAM,IAAI,CAAC,KAAK,EAAE,KAAK,SAAS,OAAO,MAAM,SAAS,IAAI,CAAC;AAAA,EAC9J;AACA,QAAM,KAAK,EAAE;AAEb,QAAM,QAAQ,KAAK,IAAI,GAAG,GAAG,OAAO,OAAO,IAAI,CAAC,MAAM,EAAE,KAAK,MAAM,CAAC;AACpE,QAAM,SAAS,IAAI,KAAK,MAAM,UAAU,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACnE,QAAM,KAAK,GAAG,QAAQ,OAAO,KAAK,CAAC,qCAAgC;AACnE,QAAM,MAAM,CAAC,OAAsB,MAAc,IAAI,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC;AAC7E,aAAW,SAAS,OAAO,QAAQ;AACjC,UAAM,IAAI,OAAO,IAAI,MAAM,IAAI;AAC/B,UAAM,SAAS,IACX,CAAC,EAAE,cAAc,QAAQ,EAAE,cAAc,IAAI,IAAI,UAAU,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE,WAAW,QAAQ,EAAE,WAAW,IAAI,IAAI,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,EAC/J,OAAO,OAAO,EACd,KAAK,GAAG,IACX;AACJ,UAAM;AAAA,MACJ,GAAG,MAAM,KAAK,OAAO,KAAK,CAAC,KAAK,IAAI,MAAM,WAAW,CAAC,CAAC,KAAK,IAAI,MAAM,QAAQ,CAAC,CAAC,MAAM,OAAO,MAAM,EAAE,EAAE,SAAS,CAAC,CAAC,KAAK,OAAO,MAAM,EAAE,EAAE,SAAS,CAAC,CAAC,KAAK,OAAO,MAAM,EAAE,EAAE,SAAS,CAAC,CAAC,KAAK,MAAM;AAAA,IACjM;AAAA,EACF;AACA,QAAM,KAAK,EAAE;AACb,QAAM;AAAA,IACJ,sBAAsB,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,GAAG,UAAU,MAAM,aAAa,IAAI,CAAC,YAAY,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG,UAAU,MAAM,UAAU,IAAI,CAAC;AAAA,EACtJ;AACA,QAAM,IAAI,EAAE;AACZ,QAAM,QAAQ,EAAE,mBAAmB,EAAE,mBACjC,gBAAgB,EAAE,gBAAgB,eAAe,OAAO,CAAC,YAAY,EAAE,iBAAiB,eAAe,OAAO,CAAC,KAC/G;AACJ,QAAM;AAAA,IACJ,WAAW,EAAE,aAAa,eAAe,OAAO,CAAC,UAAU,OAAO,MAAM,gBAAgB,IAAI,CAAC,MAAM,EAAE,iBAAiB,eAAe,OAAO,CAAC,cAAc,OAAO,MAAM,oBAAoB,IAAI,CAAC,GAAG,KAAK,KAAK,EAAE,KAAK,cAClN,EAAE,SAAS,SAAY,WAAW,EAAE,KAAK,QAAQ,CAAC,CAAC,GAAG,OAAO,MAAM,QAAQ,MAAM,CAAC,CAAC,KAAK;AAAA,EAC7F;AACA,QAAM;AAAA,IACJ,gBAAgB,EAAE,UAAU,GAAG,KAAK,OAAO,MAAM,SAAS,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,WAAW,EAAE,UAAU,GAAG;AAAA,EAC5H;AAEA,QAAM,SAAS,OAAO,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE;AAC/C,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,SAAS;AACpB,eAAW,QAAQ,OAAQ,OAAM,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,EAAE;AAAA,EACvE;AACA,QAAM,YAAY,OAAO,MAAM,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK;AAC7D,MAAI,UAAU,SAAS,GAAG;AACxB,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,aAAa;AACxB,eAAW,QAAQ,WAAW;AAC5B,iBAAW,QAAQ,KAAK,OAAO,OAAO,CAAC,MAAM,EAAE,YAAY,OAAO,GAAG;AACnE,cAAM,SACJ,KAAK,YAAY,UACb,YAAY,KAAK,UAAU,KAAK,QAAQ,CAAC,SAAS,KAAK,UAAU,KAAK,MAAM,CAAC,KAC7E,KAAK,YAAY,YACf,YAAY,KAAK,UAAU,KAAK,QAAQ,CAAC,kBACzC,cAAc,KAAK,UAAU,KAAK,MAAM,CAAC;AACjD,cAAM,OAAO,KAAK,aAAa,KAAK,KAAK,UAAU,MAAM;AACzD,cAAM,KAAK,KAAK,KAAK,IAAI,WAAM,KAAK,IAAI,KAAK,MAAM,GAAG,IAAI,EAAE;AAAA,MAC9D;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;","names":["import_node_fs","import_node_path","stable"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/replay.ts","../src/eval.ts"],"sourcesContent":["export {\n RecordingProvider,\n ReplayProvider,\n ReplayMissError,\n replayOrRecord,\n recordingKey,\n recordingPath,\n} from \"./replay.js\";\nexport type { Recording, ReplayOptions } from \"./replay.js\";\n\nexport {\n runEval,\n loadFixtures,\n compareLeaves,\n flattenLeaves,\n leavesEqual,\n fieldStats,\n estimateCost,\n saveReport,\n loadReport,\n diffReports,\n formatReport,\n} from \"./eval.js\";\nexport type {\n EvalFixture,\n EvalOptions,\n EvalReport,\n EvalItem,\n EvalTotals,\n EvalDiff,\n FieldDelta,\n FieldStats,\n LeafResult,\n LeafOutcome,\n TokenPrices,\n Usage,\n} from \"./eval.js\";\n","import { createHash } from \"node:crypto\";\nimport { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Provider, ProviderRequest, ProviderResponse } from \"@sembl/core\";\n\n/** What a recording holds: enough of the request to recognise it, and the answer. */\nexport interface Recording {\n /** Hash of the parts of the request that determine the answer. */\n key: string;\n schemaId: string;\n request: {\n systemPrompt: string;\n userInput: string;\n jsonSchema: Record<string, unknown>;\n history?: ProviderRequest[\"history\"];\n };\n response: ProviderResponse;\n recordedAt: string;\n}\n\n/** JSON with sorted keys, so the same request always hashes the same. */\nfunction stable(value: unknown): string {\n return JSON.stringify(value, (_key, v) =>\n v && typeof v === \"object\" && !Array.isArray(v)\n ? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : v,\n );\n}\n\n/**\n * The key a request records under. Only the parts that reach the model\n * count: the system prompt, the user input and the JSON Schema. The runtime\n * schema and bundle are already folded into those.\n */\nexport function recordingKey(request: ProviderRequest): string {\n const material = stable({\n systemPrompt: request.systemPrompt,\n userInput: request.userInput,\n jsonSchema: request.jsonSchema,\n // A repair call shares everything above with the call it repairs.\n ...(request.history && request.history.length > 0 ? { history: request.history } : {}),\n });\n return createHash(\"sha256\").update(material).digest(\"hex\").slice(0, 24);\n}\n\nfunction slug(id: string): string {\n return id.replace(/[^A-Za-z0-9_-]+/g, \"_\").slice(0, 40) || \"schema\";\n}\n\n/** Where a recording for a request lives inside a directory. */\nexport function recordingPath(dir: string, request: ProviderRequest): string {\n return join(dir, `${slug(request.schema.id)}.${recordingKey(request)}.json`);\n}\n\n/** Thrown by {@link ReplayProvider} when no recording matches a request. */\nexport class ReplayMissError extends Error {\n constructor(\n public readonly key: string,\n public readonly dir: string,\n public readonly schemaId: string,\n ) {\n super(\n `No recording for a \"${schemaId}\" request (key ${key}) in ${dir}. ` +\n \"Run once with a RecordingProvider, or pass a fallback provider to record misses.\",\n );\n this.name = \"ReplayMissError\";\n }\n}\n\n/**\n * Calls another provider and writes every request/response pair to a\n * directory, one JSON file each, named by schema and request hash. Run your\n * extraction code through it once with real credentials; the files it leaves\n * behind let a {@link ReplayProvider} answer the same requests offline.\n *\n * A request is identified by what reaches the model, so editing a field\n * description or the input produces a new recording rather than a stale hit.\n */\nexport class RecordingProvider implements Provider {\n readonly supportsHistory: boolean;\n\n constructor(\n private readonly inner: Provider,\n private readonly dir: string,\n ) {\n this.supportsHistory = inner.supportsHistory === true;\n mkdirSync(dir, { recursive: true });\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const response = await this.inner.complete(request);\n const recording: Recording = {\n key: recordingKey(request),\n schemaId: request.schema.id,\n request: {\n systemPrompt: request.systemPrompt,\n userInput: request.userInput,\n jsonSchema: request.jsonSchema,\n ...(request.history ? { history: request.history } : {}),\n },\n response,\n recordedAt: new Date().toISOString(),\n };\n writeFileSync(recordingPath(this.dir, request), JSON.stringify(recording, null, 2) + \"\\n\");\n return response;\n }\n}\n\n/** Options for {@link ReplayProvider}. */\nexport interface ReplayOptions {\n /**\n * Where to send a request no recording matches. The answer is recorded,\n * so the next run replays it. Without one, a miss throws\n * {@link ReplayMissError} — the right behaviour in CI, where a miss means\n * a fixture changed and nobody re-recorded.\n */\n fallback?: Provider;\n}\n\n/**\n * Answers requests from a directory of recordings, never touching the\n * network. Deterministic, free, and fast: the provider to put under tests of\n * your own extraction code.\n */\nexport class ReplayProvider implements Provider {\n private readonly recorder: RecordingProvider | undefined;\n /**\n * Follows the fallback when there is one. A strict replay claims support\n * so that a recorded multi-turn repair is asked for the same way it was\n * recorded; the recordings decide whether it is answered.\n */\n readonly supportsHistory: boolean;\n\n constructor(\n private readonly dir: string,\n options: ReplayOptions = {},\n ) {\n this.recorder = options.fallback ? new RecordingProvider(options.fallback, dir) : undefined;\n this.supportsHistory = options.fallback ? options.fallback.supportsHistory === true : true;\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const path = recordingPath(this.dir, request);\n if (existsSync(path)) {\n const recording = JSON.parse(readFileSync(path, \"utf8\")) as Recording;\n return recording.response;\n }\n if (this.recorder) {\n return this.recorder.complete(request);\n }\n throw new ReplayMissError(recordingKey(request), this.dir, request.schema.id);\n }\n\n /** How many recordings the directory holds. */\n size(): number {\n if (!existsSync(this.dir)) return 0;\n return readdirSync(this.dir).filter((f) => f.endsWith(\".json\")).length;\n }\n}\n\n/**\n * The usual arrangement: replay from `dir`, and record misses through\n * `live` when it is given — say, only when an API key is present — so the\n * same test file works locally with credentials and in CI without them.\n */\nexport function replayOrRecord(dir: string, live?: Provider): Provider {\n return new ReplayProvider(dir, live ? { fallback: live } : {});\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport {\n coerce,\n partialCoerce,\n coerceWithProvenance,\n partialCoerceWithProvenance,\n} from \"@sembl/core\";\nimport type {\n CoerceInput,\n CoerceOptions,\n FieldProvenance,\n Provider,\n ProviderRequest,\n ProviderResponse,\n ProviderUsage,\n ResolvedIssue,\n Source,\n} from \"@sembl/core\";\n\n/** One case: an input and what a correct extraction of it looks like. */\nexport interface EvalFixture {\n /** Shown in the report. Defaults to the file it was loaded from. */\n name?: string;\n input: CoerceInput;\n /** The expected extraction. Absent and `null` both mean \"not present\". */\n expected: Record<string, unknown>;\n}\n\n/** Per-million-token prices, for a cost line in the report. */\nexport interface TokenPrices {\n inputPerMTok: number;\n outputPerMTok: number;\n /** Defaults to `inputPerMTok` when the provider reports cache reads. */\n cacheReadPerMTok?: number;\n /** Defaults to `inputPerMTok` when the provider reports cache writes. */\n cacheWritePerMTok?: number;\n}\n\n/** Options for {@link runEval}. Everything in `CoerceOptions` applies per fixture. */\nexport interface EvalOptions extends CoerceOptions {\n fixtures: readonly EvalFixture[];\n /** Which coercion to run. Default `\"coerce\"`. */\n mode?: \"coerce\" | \"partialCoerce\";\n /** Ask for provenance, so the report can show confidence per field. */\n provenance?: boolean;\n /** How many fixtures run at once. Default 1, for stable latency numbers. */\n concurrency?: number;\n /** Prices for the cost line. Without them the report has no cost. */\n prices?: TokenPrices;\n}\n\n/** How one leaf compared. */\nexport type LeafOutcome = \"match\" | \"wrong\" | \"missing\" | \"extra\";\n\nexport interface LeafResult {\n path: string;\n outcome: LeafOutcome;\n expected?: unknown;\n actual?: unknown;\n confidence?: FieldProvenance[\"confidence\"];\n}\n\nexport interface EvalItem {\n name: string;\n ok: boolean;\n /** Message of the error the coercion threw, when it did. */\n error?: string;\n /** Whether every expected leaf matched and nothing extra came back. */\n exact: boolean;\n leaves: LeafResult[];\n issues: ResolvedIssue[];\n latencyMs: number;\n calls: number;\n usage: Usage;\n}\n\nexport interface Usage {\n promptTokens: number;\n completionTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n}\n\nexport interface FieldStats {\n path: string;\n tp: number;\n fp: number;\n fn: number;\n /** `tp / (tp + fp)`; null when nothing was returned for the field. */\n precision: number | null;\n /** `tp / (tp + fn)`; null when nothing was expected for the field. */\n recall: number | null;\n}\n\nexport interface EvalTotals {\n fixtures: number;\n ok: number;\n exact: number;\n precision: number | null;\n recall: number | null;\n usage: Usage;\n calls: number;\n cost?: number;\n latencyMs: { p50: number; p95: number; max: number; total: number };\n}\n\nexport interface EvalReport {\n schemaId: string;\n mode: \"coerce\" | \"partialCoerce\";\n ranAt: string;\n items: EvalItem[];\n fields: FieldStats[];\n totals: EvalTotals;\n}\n\n// ---------------------------------------------------------------------------\n// Fixtures\n\ninterface FileInput {\n file: string;\n label?: string;\n}\n\nfunction isFileInput(value: unknown): value is FileInput {\n return typeof value === \"object\" && value !== null && typeof (value as FileInput).file === \"string\";\n}\n\n/** Resolve `{ file, label }` inputs against the fixture's own directory. */\nfunction resolveInput(input: unknown, baseDir: string): CoerceInput {\n const one = (value: unknown): Source | string => {\n if (typeof value === \"string\") return value;\n if (isFileInput(value)) {\n const text = readFileSync(resolve(baseDir, value.file), \"utf8\");\n return value.label ? { label: value.label, text } : { text };\n }\n return value as Source;\n };\n if (Array.isArray(input)) {\n return input.map((v) => {\n const r = one(v);\n return typeof r === \"string\" ? { text: r } : r;\n });\n }\n return one(input);\n}\n\n/**\n * Load fixtures from a directory: every `*.json` file is either one fixture\n * or an array of them. An input may be `{ \"file\": \"page.html\", \"label\": … }`\n * to pull text from a sibling file, which keeps large scraped pages out of\n * the JSON.\n */\nexport function loadFixtures(dir: string): EvalFixture[] {\n const root = resolve(dir);\n if (!existsSync(root)) {\n throw new Error(`Fixture directory not found: ${root}`);\n }\n const fixtures: EvalFixture[] = [];\n const files = readdirSync(root)\n .filter((f) => f.endsWith(\".json\") && !f.startsWith(\".\"))\n .sort();\n for (const file of files) {\n const path = join(root, file);\n const parsed = JSON.parse(readFileSync(path, \"utf8\")) as unknown;\n const list = Array.isArray(parsed) ? parsed : [parsed];\n list.forEach((raw, index) => {\n const fixture = raw as EvalFixture;\n if (!fixture || typeof fixture !== \"object\" || !(\"input\" in fixture) || !(\"expected\" in fixture)) {\n throw new Error(`${path}${list.length > 1 ? `[${index}]` : \"\"}: a fixture needs \"input\" and \"expected\"`);\n }\n const stem = basename(file, \".json\");\n fixtures.push({\n name: fixture.name ?? (list.length > 1 ? `${stem}[${index}]` : stem),\n input: resolveInput(fixture.input, dirname(path)),\n expected: fixture.expected,\n });\n });\n }\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Scoring\n\nfunction stable(value: unknown): string {\n return JSON.stringify(value, (_k, v) =>\n v && typeof v === \"object\" && !Array.isArray(v)\n ? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : v,\n );\n}\n\nfunction isPrimitive(value: unknown): boolean {\n return value === null || typeof value !== \"object\";\n}\n\n/**\n * Flatten a value to its leaves, keyed by dotted path. Objects recurse;\n * arrays are leaves. `null` and `undefined` are absence, not leaves.\n */\nexport function flattenLeaves(value: unknown, prefix = \"\"): Map<string, unknown> {\n const leaves = new Map<string, unknown>();\n if (value === null || value === undefined) return leaves;\n if (typeof value !== \"object\" || Array.isArray(value)) {\n leaves.set(prefix, value);\n return leaves;\n }\n for (const [key, child] of Object.entries(value as Record<string, unknown>)) {\n const path = prefix ? `${prefix}.${key}` : key;\n for (const [leafPath, leaf] of flattenLeaves(child, path)) {\n leaves.set(leafPath, leaf);\n }\n }\n return leaves;\n}\n\n/**\n * Whether two leaves agree. Arrays of primitives compare as multisets —\n * the order amenities come back in is not a fact about the listing —\n * and everything else compares structurally.\n */\nexport function leavesEqual(a: unknown, b: unknown): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n if (a.every(isPrimitive) && b.every(isPrimitive)) {\n const sortedA = [...a].map(String).sort();\n const sortedB = [...b].map(String).sort();\n return sortedA.every((v, i) => v === sortedB[i]);\n }\n }\n return stable(a) === stable(b);\n}\n\n/** Compare one extraction against its expectation, leaf by leaf. */\nexport function compareLeaves(\n expected: Record<string, unknown>,\n actual: Record<string, unknown> | undefined,\n provenance: Record<string, FieldProvenance> = {},\n): LeafResult[] {\n const want = flattenLeaves(expected);\n const got = flattenLeaves(actual ?? {});\n const paths = [...new Set([...want.keys(), ...got.keys()])].sort();\n return paths.map((path) => {\n const top = path.split(\".\")[0];\n const confidence = provenance[top]?.confidence;\n const result: LeafResult = { path, outcome: \"match\" };\n if (confidence) result.confidence = confidence;\n if (want.has(path) && got.has(path)) {\n result.expected = want.get(path);\n result.actual = got.get(path);\n result.outcome = leavesEqual(want.get(path), got.get(path)) ? \"match\" : \"wrong\";\n } else if (want.has(path)) {\n result.expected = want.get(path);\n result.outcome = \"missing\";\n } else {\n result.actual = got.get(path);\n result.outcome = \"extra\";\n }\n return result;\n });\n}\n\nfunction ratio(num: number, den: number): number | null {\n return den === 0 ? null : num / den;\n}\n\n/** Aggregate leaf outcomes into per-field precision and recall. */\nexport function fieldStats(items: readonly EvalItem[]): FieldStats[] {\n const byPath = new Map<string, { tp: number; fp: number; fn: number }>();\n for (const item of items) {\n for (const leaf of item.leaves) {\n const stats = byPath.get(leaf.path) ?? { tp: 0, fp: 0, fn: 0 };\n switch (leaf.outcome) {\n case \"match\":\n stats.tp += 1;\n break;\n case \"wrong\":\n stats.fp += 1;\n stats.fn += 1;\n break;\n case \"missing\":\n stats.fn += 1;\n break;\n case \"extra\":\n stats.fp += 1;\n break;\n }\n byPath.set(leaf.path, stats);\n }\n }\n return [...byPath.entries()]\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([path, { tp, fp, fn }]) => ({\n path,\n tp,\n fp,\n fn,\n precision: ratio(tp, tp + fp),\n recall: ratio(tp, tp + fn),\n }));\n}\n\nfunction percentile(sorted: readonly number[], p: number): number {\n if (sorted.length === 0) return 0;\n const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);\n return sorted[Math.max(0, index)];\n}\n\nfunction emptyUsage(): Usage {\n return { promptTokens: 0, completionTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };\n}\n\nfunction addUsage(into: Usage, usage: ProviderUsage | undefined): void {\n if (!usage) return;\n into.promptTokens += usage.promptTokens;\n into.completionTokens += usage.completionTokens;\n into.cacheReadTokens += usage.cacheReadTokens ?? 0;\n into.cacheWriteTokens += usage.cacheWriteTokens ?? 0;\n}\n\n/** Dollars, from per-million-token prices. */\nexport function estimateCost(usage: Usage, prices: TokenPrices): number {\n const per = (tokens: number, price: number) => (tokens / 1_000_000) * price;\n return (\n per(usage.promptTokens, prices.inputPerMTok) +\n per(usage.completionTokens, prices.outputPerMTok) +\n per(usage.cacheReadTokens, prices.cacheReadPerMTok ?? prices.inputPerMTok) +\n per(usage.cacheWriteTokens, prices.cacheWritePerMTok ?? prices.inputPerMTok)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Running\n\ninterface ItemContext {\n usage: Usage;\n calls: number;\n}\n\nconst context = new AsyncLocalStorage<ItemContext>();\n\n/** Credits each call's usage to whichever fixture is running it. */\nclass MeteredProvider implements Provider {\n readonly supportsHistory: boolean;\n\n constructor(private readonly inner: Provider) {\n this.supportsHistory = inner.supportsHistory === true;\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const response = await this.inner.complete(request);\n const ctx = context.getStore();\n if (ctx) {\n ctx.calls += 1;\n addUsage(ctx.usage, response.usage);\n }\n return response;\n }\n}\n\nasync function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results: R[] = new Array(items.length);\n let next = 0;\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {\n while (next < items.length) {\n const index = next++;\n results[index] = await fn(items[index], index);\n }\n });\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Run every fixture through a coercion and score the results.\n *\n * Per-field precision and recall make a description change measurable: a\n * field whose recall dropped after a rewording is a field the rewording\n * hurt. Token usage and latency come along so the cost of a change is\n * visible next to its effect. Pair it with a `ReplayProvider` to run in CI\n * without spend.\n */\nexport async function runEval(options: EvalOptions): Promise<EvalReport> {\n const {\n fixtures,\n mode = \"coerce\",\n provenance = false,\n concurrency = 1,\n prices,\n provider,\n ...coerceOptions\n } = options;\n const metered = new MeteredProvider(provider);\n const run = mode === \"coerce\"\n ? provenance ? coerceWithProvenance : coerce\n : provenance ? partialCoerceWithProvenance : partialCoerce;\n\n const items = await mapWithConcurrency(fixtures, concurrency, async (fixture, index) => {\n const ctx: ItemContext = { usage: emptyUsage(), calls: 0 };\n const name = fixture.name ?? `fixture ${index + 1}`;\n const started = performance.now();\n return context.run(ctx, async (): Promise<EvalItem> => {\n try {\n const result = await run<Record<string, unknown>>(fixture.input, {\n ...coerceOptions,\n provider: metered,\n });\n const data = provenance\n ? (result as { data: Record<string, unknown> }).data\n : (result as Record<string, unknown>);\n const prov = provenance ? (result as { provenance: Record<string, FieldProvenance> }).provenance : {};\n const issues = provenance ? (result as { issues: ResolvedIssue[] }).issues : [];\n const leaves = compareLeaves(fixture.expected, data, prov);\n return {\n name,\n ok: true,\n exact: leaves.every((l) => l.outcome === \"match\"),\n leaves,\n issues,\n latencyMs: Math.round(performance.now() - started),\n calls: ctx.calls,\n usage: ctx.usage,\n };\n } catch (error) {\n return {\n name,\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n exact: false,\n leaves: compareLeaves(fixture.expected, undefined),\n issues: [],\n latencyMs: Math.round(performance.now() - started),\n calls: ctx.calls,\n usage: ctx.usage,\n };\n }\n });\n });\n\n const fields = fieldStats(items);\n const tp = fields.reduce((s, f) => s + f.tp, 0);\n const fp = fields.reduce((s, f) => s + f.fp, 0);\n const fn = fields.reduce((s, f) => s + f.fn, 0);\n const usage = emptyUsage();\n for (const item of items) {\n usage.promptTokens += item.usage.promptTokens;\n usage.completionTokens += item.usage.completionTokens;\n usage.cacheReadTokens += item.usage.cacheReadTokens;\n usage.cacheWriteTokens += item.usage.cacheWriteTokens;\n }\n const latencies = items.map((i) => i.latencyMs).sort((a, b) => a - b);\n\n return {\n schemaId: options.schema.id,\n mode,\n ranAt: new Date().toISOString(),\n items,\n fields,\n totals: {\n fixtures: items.length,\n ok: items.filter((i) => i.ok).length,\n exact: items.filter((i) => i.exact).length,\n precision: ratio(tp, tp + fp),\n recall: ratio(tp, tp + fn),\n usage,\n calls: items.reduce((s, i) => s + i.calls, 0),\n ...(prices ? { cost: estimateCost(usage, prices) } : {}),\n latencyMs: {\n p50: percentile(latencies, 50),\n p95: percentile(latencies, 95),\n max: latencies[latencies.length - 1] ?? 0,\n total: latencies.reduce((s, l) => s + l, 0),\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Persistence and diffs\n\nexport function saveReport(report: EvalReport, file: string): void {\n mkdirSync(dirname(resolve(file)), { recursive: true });\n writeFileSync(resolve(file), JSON.stringify(report, null, 2) + \"\\n\");\n}\n\nexport function loadReport(file: string): EvalReport | undefined {\n const path = resolve(file);\n if (!existsSync(path)) return undefined;\n return JSON.parse(readFileSync(path, \"utf8\")) as EvalReport;\n}\n\nexport interface FieldDelta {\n path: string;\n precision: number | null;\n recall: number | null;\n}\n\n/** What changed between two runs. Deltas are `next − previous`. */\nexport interface EvalDiff {\n exact: number;\n ok: number;\n precision: number | null;\n recall: number | null;\n promptTokens: number;\n completionTokens: number;\n cost?: number;\n p50Ms: number;\n /** Fields whose precision or recall moved, largest drop first. */\n fields: FieldDelta[];\n}\n\nfunction delta(a: number | null | undefined, b: number | null | undefined): number | null {\n return a === null || a === undefined || b === null || b === undefined ? null : b - a;\n}\n\nexport function diffReports(previous: EvalReport, next: EvalReport): EvalDiff {\n const prevFields = new Map(previous.fields.map((f) => [f.path, f]));\n const fields: FieldDelta[] = [];\n for (const field of next.fields) {\n const before = prevFields.get(field.path);\n const precision = delta(before?.precision, field.precision);\n const recall = delta(before?.recall, field.recall);\n if ((precision !== null && precision !== 0) || (recall !== null && recall !== 0)) {\n fields.push({ path: field.path, precision, recall });\n }\n }\n fields.sort((a, b) => Math.min(a.precision ?? 0, a.recall ?? 0) - Math.min(b.precision ?? 0, b.recall ?? 0));\n\n return {\n exact: next.totals.exact - previous.totals.exact,\n ok: next.totals.ok - previous.totals.ok,\n precision: delta(previous.totals.precision, next.totals.precision),\n recall: delta(previous.totals.recall, next.totals.recall),\n promptTokens: next.totals.usage.promptTokens - previous.totals.usage.promptTokens,\n completionTokens: next.totals.usage.completionTokens - previous.totals.usage.completionTokens,\n ...(next.totals.cost !== undefined && previous.totals.cost !== undefined\n ? { cost: next.totals.cost - previous.totals.cost }\n : {}),\n p50Ms: next.totals.latencyMs.p50 - previous.totals.latencyMs.p50,\n fields,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Formatting\n\nfunction pct(value: number | null): string {\n return value === null ? \" – \" : `${(value * 100).toFixed(0).padStart(3)}%`;\n}\n\nfunction signed(value: number | null, digits = 0, suffix = \"\"): string {\n if (value === null || value === 0) return \"\";\n const text = digits > 0 ? value.toFixed(digits) : String(value);\n return ` (${value > 0 ? \"+\" : \"\"}${text}${suffix})`;\n}\n\nfunction signedPct(value: number | null): string {\n return value === null || value === 0 ? \"\" : ` (${value > 0 ? \"+\" : \"\"}${(value * 100).toFixed(0)}pt)`;\n}\n\n/** A plain-text report for a terminal, with deltas when a previous run is given. */\nexport function formatReport(report: EvalReport, diff?: EvalDiff): string {\n const t = report.totals;\n const lines: string[] = [];\n lines.push(\n `Eval: ${report.schemaId} (${report.mode}) — ${t.fixtures} fixture(s), ${t.ok} ran${signed(diff?.ok ?? null)}, ${t.exact} exact${signed(diff?.exact ?? null)}`,\n );\n lines.push(\"\");\n\n const width = Math.max(5, ...report.fields.map((f) => f.path.length));\n const deltas = new Map((diff?.fields ?? []).map((f) => [f.path, f]));\n lines.push(`${\"Field\".padEnd(width)} Prec Recall tp fp fn Δ`);\n const col = (value: number | null, w: number) => pct(value).trim().padStart(w);\n for (const field of report.fields) {\n const d = deltas.get(field.path);\n const change = d\n ? [d.precision !== null && d.precision !== 0 ? `P${signedPct(d.precision).trim()}` : \"\", d.recall !== null && d.recall !== 0 ? `R${signedPct(d.recall).trim()}` : \"\"]\n .filter(Boolean)\n .join(\" \")\n : \"\";\n lines.push(\n `${field.path.padEnd(width)} ${col(field.precision, 4)} ${col(field.recall, 6)} ${String(field.tp).padStart(2)} ${String(field.fp).padStart(2)} ${String(field.fn).padStart(2)} ${change}`,\n );\n }\n lines.push(\"\");\n lines.push(\n `Overall: precision ${pct(t.precision).trim()}${signedPct(diff?.precision ?? null)}, recall ${pct(t.recall).trim()}${signedPct(diff?.recall ?? null)}`,\n );\n const u = t.usage;\n const cache = u.cacheReadTokens || u.cacheWriteTokens\n ? `, cache read ${u.cacheReadTokens.toLocaleString(\"en-US\")} / write ${u.cacheWriteTokens.toLocaleString(\"en-US\")}`\n : \"\";\n lines.push(\n `Tokens: ${u.promptTokens.toLocaleString(\"en-US\")} prompt${signed(diff?.promptTokens ?? null)} / ${u.completionTokens.toLocaleString(\"en-US\")} completion${signed(diff?.completionTokens ?? null)}${cache}; ${t.calls} call(s)` +\n (t.cost !== undefined ? `; cost $${t.cost.toFixed(4)}${signed(diff?.cost ?? null, 4)}` : \"\"),\n );\n lines.push(\n `Latency: p50 ${t.latencyMs.p50}ms${signed(diff?.p50Ms ?? null, 0, \"ms\")}, p95 ${t.latencyMs.p95}ms, max ${t.latencyMs.max}ms`,\n );\n\n const failed = report.items.filter((i) => !i.ok);\n if (failed.length > 0) {\n lines.push(\"\");\n lines.push(\"Failed:\");\n for (const item of failed) lines.push(` ${item.name}: ${item.error}`);\n }\n const imperfect = report.items.filter((i) => i.ok && !i.exact);\n if (imperfect.length > 0) {\n lines.push(\"\");\n lines.push(\"Mismatches:\");\n for (const item of imperfect) {\n for (const leaf of item.leaves.filter((l) => l.outcome !== \"match\")) {\n const detail =\n leaf.outcome === \"wrong\"\n ? `expected ${JSON.stringify(leaf.expected)}, got ${JSON.stringify(leaf.actual)}`\n : leaf.outcome === \"missing\"\n ? `expected ${JSON.stringify(leaf.expected)}, got nothing`\n : `unexpected ${JSON.stringify(leaf.actual)}`;\n const conf = leaf.confidence ? ` [${leaf.confidence}]` : \"\";\n lines.push(` ${item.name} › ${leaf.path}: ${detail}${conf}`);\n }\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":["import_node_fs","import_node_path","stable"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProviderResponse, Provider,
|
|
1
|
+
import { ProviderRequest, ProviderResponse, Provider, CoerceInput, FieldProvenance, ResolvedIssue, CoerceOptions } from '@sembl/core';
|
|
2
2
|
|
|
3
3
|
/** What a recording holds: enough of the request to recognise it, and the answer. */
|
|
4
4
|
interface Recording {
|
|
@@ -9,6 +9,7 @@ interface Recording {
|
|
|
9
9
|
systemPrompt: string;
|
|
10
10
|
userInput: string;
|
|
11
11
|
jsonSchema: Record<string, unknown>;
|
|
12
|
+
history?: ProviderRequest["history"];
|
|
12
13
|
};
|
|
13
14
|
response: ProviderResponse;
|
|
14
15
|
recordedAt: string;
|
|
@@ -40,6 +41,7 @@ declare class ReplayMissError extends Error {
|
|
|
40
41
|
declare class RecordingProvider implements Provider {
|
|
41
42
|
private readonly inner;
|
|
42
43
|
private readonly dir;
|
|
44
|
+
readonly supportsHistory: boolean;
|
|
43
45
|
constructor(inner: Provider, dir: string);
|
|
44
46
|
complete(request: ProviderRequest): Promise<ProviderResponse>;
|
|
45
47
|
}
|
|
@@ -61,6 +63,12 @@ interface ReplayOptions {
|
|
|
61
63
|
declare class ReplayProvider implements Provider {
|
|
62
64
|
private readonly dir;
|
|
63
65
|
private readonly recorder;
|
|
66
|
+
/**
|
|
67
|
+
* Follows the fallback when there is one. A strict replay claims support
|
|
68
|
+
* so that a recorded multi-turn repair is asked for the same way it was
|
|
69
|
+
* recorded; the recordings decide whether it is answered.
|
|
70
|
+
*/
|
|
71
|
+
readonly supportsHistory: boolean;
|
|
64
72
|
constructor(dir: string, options?: ReplayOptions);
|
|
65
73
|
complete(request: ProviderRequest): Promise<ProviderResponse>;
|
|
66
74
|
/** How many recordings the directory holds. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProviderResponse, Provider,
|
|
1
|
+
import { ProviderRequest, ProviderResponse, Provider, CoerceInput, FieldProvenance, ResolvedIssue, CoerceOptions } from '@sembl/core';
|
|
2
2
|
|
|
3
3
|
/** What a recording holds: enough of the request to recognise it, and the answer. */
|
|
4
4
|
interface Recording {
|
|
@@ -9,6 +9,7 @@ interface Recording {
|
|
|
9
9
|
systemPrompt: string;
|
|
10
10
|
userInput: string;
|
|
11
11
|
jsonSchema: Record<string, unknown>;
|
|
12
|
+
history?: ProviderRequest["history"];
|
|
12
13
|
};
|
|
13
14
|
response: ProviderResponse;
|
|
14
15
|
recordedAt: string;
|
|
@@ -40,6 +41,7 @@ declare class ReplayMissError extends Error {
|
|
|
40
41
|
declare class RecordingProvider implements Provider {
|
|
41
42
|
private readonly inner;
|
|
42
43
|
private readonly dir;
|
|
44
|
+
readonly supportsHistory: boolean;
|
|
43
45
|
constructor(inner: Provider, dir: string);
|
|
44
46
|
complete(request: ProviderRequest): Promise<ProviderResponse>;
|
|
45
47
|
}
|
|
@@ -61,6 +63,12 @@ interface ReplayOptions {
|
|
|
61
63
|
declare class ReplayProvider implements Provider {
|
|
62
64
|
private readonly dir;
|
|
63
65
|
private readonly recorder;
|
|
66
|
+
/**
|
|
67
|
+
* Follows the fallback when there is one. A strict replay claims support
|
|
68
|
+
* so that a recorded multi-turn repair is asked for the same way it was
|
|
69
|
+
* recorded; the recordings decide whether it is answered.
|
|
70
|
+
*/
|
|
71
|
+
readonly supportsHistory: boolean;
|
|
64
72
|
constructor(dir: string, options?: ReplayOptions);
|
|
65
73
|
complete(request: ProviderRequest): Promise<ProviderResponse>;
|
|
66
74
|
/** How many recordings the directory holds. */
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,9 @@ function recordingKey(request) {
|
|
|
12
12
|
const material = stable({
|
|
13
13
|
systemPrompt: request.systemPrompt,
|
|
14
14
|
userInput: request.userInput,
|
|
15
|
-
jsonSchema: request.jsonSchema
|
|
15
|
+
jsonSchema: request.jsonSchema,
|
|
16
|
+
// A repair call shares everything above with the call it repairs.
|
|
17
|
+
...request.history && request.history.length > 0 ? { history: request.history } : {}
|
|
16
18
|
});
|
|
17
19
|
return createHash("sha256").update(material).digest("hex").slice(0, 24);
|
|
18
20
|
}
|
|
@@ -37,8 +39,10 @@ var RecordingProvider = class {
|
|
|
37
39
|
constructor(inner, dir) {
|
|
38
40
|
this.inner = inner;
|
|
39
41
|
this.dir = dir;
|
|
42
|
+
this.supportsHistory = inner.supportsHistory === true;
|
|
40
43
|
mkdirSync(dir, { recursive: true });
|
|
41
44
|
}
|
|
45
|
+
supportsHistory;
|
|
42
46
|
async complete(request) {
|
|
43
47
|
const response = await this.inner.complete(request);
|
|
44
48
|
const recording = {
|
|
@@ -47,7 +51,8 @@ var RecordingProvider = class {
|
|
|
47
51
|
request: {
|
|
48
52
|
systemPrompt: request.systemPrompt,
|
|
49
53
|
userInput: request.userInput,
|
|
50
|
-
jsonSchema: request.jsonSchema
|
|
54
|
+
jsonSchema: request.jsonSchema,
|
|
55
|
+
...request.history ? { history: request.history } : {}
|
|
51
56
|
},
|
|
52
57
|
response,
|
|
53
58
|
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -60,8 +65,15 @@ var ReplayProvider = class {
|
|
|
60
65
|
constructor(dir, options = {}) {
|
|
61
66
|
this.dir = dir;
|
|
62
67
|
this.recorder = options.fallback ? new RecordingProvider(options.fallback, dir) : void 0;
|
|
68
|
+
this.supportsHistory = options.fallback ? options.fallback.supportsHistory === true : true;
|
|
63
69
|
}
|
|
64
70
|
recorder;
|
|
71
|
+
/**
|
|
72
|
+
* Follows the fallback when there is one. A strict replay claims support
|
|
73
|
+
* so that a recorded multi-turn repair is asked for the same way it was
|
|
74
|
+
* recorded; the recordings decide whether it is answered.
|
|
75
|
+
*/
|
|
76
|
+
supportsHistory;
|
|
65
77
|
async complete(request) {
|
|
66
78
|
const path = recordingPath(this.dir, request);
|
|
67
79
|
if (existsSync(path)) {
|
|
@@ -255,7 +267,9 @@ var context = new AsyncLocalStorage();
|
|
|
255
267
|
var MeteredProvider = class {
|
|
256
268
|
constructor(inner) {
|
|
257
269
|
this.inner = inner;
|
|
270
|
+
this.supportsHistory = inner.supportsHistory === true;
|
|
258
271
|
}
|
|
272
|
+
supportsHistory;
|
|
259
273
|
async complete(request) {
|
|
260
274
|
const response = await this.inner.complete(request);
|
|
261
275
|
const ctx = context.getStore();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/replay.ts","../src/eval.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Provider, ProviderRequest, ProviderResponse } from \"@sembl/core\";\n\n/** What a recording holds: enough of the request to recognise it, and the answer. */\nexport interface Recording {\n /** Hash of the parts of the request that determine the answer. */\n key: string;\n schemaId: string;\n request: {\n systemPrompt: string;\n userInput: string;\n jsonSchema: Record<string, unknown>;\n };\n response: ProviderResponse;\n recordedAt: string;\n}\n\n/** JSON with sorted keys, so the same request always hashes the same. */\nfunction stable(value: unknown): string {\n return JSON.stringify(value, (_key, v) =>\n v && typeof v === \"object\" && !Array.isArray(v)\n ? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : v,\n );\n}\n\n/**\n * The key a request records under. Only the parts that reach the model\n * count: the system prompt, the user input and the JSON Schema. The runtime\n * schema and bundle are already folded into those.\n */\nexport function recordingKey(request: ProviderRequest): string {\n const material = stable({\n systemPrompt: request.systemPrompt,\n userInput: request.userInput,\n jsonSchema: request.jsonSchema,\n });\n return createHash(\"sha256\").update(material).digest(\"hex\").slice(0, 24);\n}\n\nfunction slug(id: string): string {\n return id.replace(/[^A-Za-z0-9_-]+/g, \"_\").slice(0, 40) || \"schema\";\n}\n\n/** Where a recording for a request lives inside a directory. */\nexport function recordingPath(dir: string, request: ProviderRequest): string {\n return join(dir, `${slug(request.schema.id)}.${recordingKey(request)}.json`);\n}\n\n/** Thrown by {@link ReplayProvider} when no recording matches a request. */\nexport class ReplayMissError extends Error {\n constructor(\n public readonly key: string,\n public readonly dir: string,\n public readonly schemaId: string,\n ) {\n super(\n `No recording for a \"${schemaId}\" request (key ${key}) in ${dir}. ` +\n \"Run once with a RecordingProvider, or pass a fallback provider to record misses.\",\n );\n this.name = \"ReplayMissError\";\n }\n}\n\n/**\n * Calls another provider and writes every request/response pair to a\n * directory, one JSON file each, named by schema and request hash. Run your\n * extraction code through it once with real credentials; the files it leaves\n * behind let a {@link ReplayProvider} answer the same requests offline.\n *\n * A request is identified by what reaches the model, so editing a field\n * description or the input produces a new recording rather than a stale hit.\n */\nexport class RecordingProvider implements Provider {\n constructor(\n private readonly inner: Provider,\n private readonly dir: string,\n ) {\n mkdirSync(dir, { recursive: true });\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const response = await this.inner.complete(request);\n const recording: Recording = {\n key: recordingKey(request),\n schemaId: request.schema.id,\n request: {\n systemPrompt: request.systemPrompt,\n userInput: request.userInput,\n jsonSchema: request.jsonSchema,\n },\n response,\n recordedAt: new Date().toISOString(),\n };\n writeFileSync(recordingPath(this.dir, request), JSON.stringify(recording, null, 2) + \"\\n\");\n return response;\n }\n}\n\n/** Options for {@link ReplayProvider}. */\nexport interface ReplayOptions {\n /**\n * Where to send a request no recording matches. The answer is recorded,\n * so the next run replays it. Without one, a miss throws\n * {@link ReplayMissError} — the right behaviour in CI, where a miss means\n * a fixture changed and nobody re-recorded.\n */\n fallback?: Provider;\n}\n\n/**\n * Answers requests from a directory of recordings, never touching the\n * network. Deterministic, free, and fast: the provider to put under tests of\n * your own extraction code.\n */\nexport class ReplayProvider implements Provider {\n private readonly recorder: RecordingProvider | undefined;\n\n constructor(\n private readonly dir: string,\n options: ReplayOptions = {},\n ) {\n this.recorder = options.fallback ? new RecordingProvider(options.fallback, dir) : undefined;\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const path = recordingPath(this.dir, request);\n if (existsSync(path)) {\n const recording = JSON.parse(readFileSync(path, \"utf8\")) as Recording;\n return recording.response;\n }\n if (this.recorder) {\n return this.recorder.complete(request);\n }\n throw new ReplayMissError(recordingKey(request), this.dir, request.schema.id);\n }\n\n /** How many recordings the directory holds. */\n size(): number {\n if (!existsSync(this.dir)) return 0;\n return readdirSync(this.dir).filter((f) => f.endsWith(\".json\")).length;\n }\n}\n\n/**\n * The usual arrangement: replay from `dir`, and record misses through\n * `live` when it is given — say, only when an API key is present — so the\n * same test file works locally with credentials and in CI without them.\n */\nexport function replayOrRecord(dir: string, live?: Provider): Provider {\n return new ReplayProvider(dir, live ? { fallback: live } : {});\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport {\n coerce,\n partialCoerce,\n coerceWithProvenance,\n partialCoerceWithProvenance,\n} from \"@sembl/core\";\nimport type {\n CoerceInput,\n CoerceOptions,\n FieldProvenance,\n Provider,\n ProviderRequest,\n ProviderResponse,\n ProviderUsage,\n ResolvedIssue,\n Source,\n} from \"@sembl/core\";\n\n/** One case: an input and what a correct extraction of it looks like. */\nexport interface EvalFixture {\n /** Shown in the report. Defaults to the file it was loaded from. */\n name?: string;\n input: CoerceInput;\n /** The expected extraction. Absent and `null` both mean \"not present\". */\n expected: Record<string, unknown>;\n}\n\n/** Per-million-token prices, for a cost line in the report. */\nexport interface TokenPrices {\n inputPerMTok: number;\n outputPerMTok: number;\n /** Defaults to `inputPerMTok` when the provider reports cache reads. */\n cacheReadPerMTok?: number;\n /** Defaults to `inputPerMTok` when the provider reports cache writes. */\n cacheWritePerMTok?: number;\n}\n\n/** Options for {@link runEval}. Everything in `CoerceOptions` applies per fixture. */\nexport interface EvalOptions extends CoerceOptions {\n fixtures: readonly EvalFixture[];\n /** Which coercion to run. Default `\"coerce\"`. */\n mode?: \"coerce\" | \"partialCoerce\";\n /** Ask for provenance, so the report can show confidence per field. */\n provenance?: boolean;\n /** How many fixtures run at once. Default 1, for stable latency numbers. */\n concurrency?: number;\n /** Prices for the cost line. Without them the report has no cost. */\n prices?: TokenPrices;\n}\n\n/** How one leaf compared. */\nexport type LeafOutcome = \"match\" | \"wrong\" | \"missing\" | \"extra\";\n\nexport interface LeafResult {\n path: string;\n outcome: LeafOutcome;\n expected?: unknown;\n actual?: unknown;\n confidence?: FieldProvenance[\"confidence\"];\n}\n\nexport interface EvalItem {\n name: string;\n ok: boolean;\n /** Message of the error the coercion threw, when it did. */\n error?: string;\n /** Whether every expected leaf matched and nothing extra came back. */\n exact: boolean;\n leaves: LeafResult[];\n issues: ResolvedIssue[];\n latencyMs: number;\n calls: number;\n usage: Usage;\n}\n\nexport interface Usage {\n promptTokens: number;\n completionTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n}\n\nexport interface FieldStats {\n path: string;\n tp: number;\n fp: number;\n fn: number;\n /** `tp / (tp + fp)`; null when nothing was returned for the field. */\n precision: number | null;\n /** `tp / (tp + fn)`; null when nothing was expected for the field. */\n recall: number | null;\n}\n\nexport interface EvalTotals {\n fixtures: number;\n ok: number;\n exact: number;\n precision: number | null;\n recall: number | null;\n usage: Usage;\n calls: number;\n cost?: number;\n latencyMs: { p50: number; p95: number; max: number; total: number };\n}\n\nexport interface EvalReport {\n schemaId: string;\n mode: \"coerce\" | \"partialCoerce\";\n ranAt: string;\n items: EvalItem[];\n fields: FieldStats[];\n totals: EvalTotals;\n}\n\n// ---------------------------------------------------------------------------\n// Fixtures\n\ninterface FileInput {\n file: string;\n label?: string;\n}\n\nfunction isFileInput(value: unknown): value is FileInput {\n return typeof value === \"object\" && value !== null && typeof (value as FileInput).file === \"string\";\n}\n\n/** Resolve `{ file, label }` inputs against the fixture's own directory. */\nfunction resolveInput(input: unknown, baseDir: string): CoerceInput {\n const one = (value: unknown): Source | string => {\n if (typeof value === \"string\") return value;\n if (isFileInput(value)) {\n const text = readFileSync(resolve(baseDir, value.file), \"utf8\");\n return value.label ? { label: value.label, text } : { text };\n }\n return value as Source;\n };\n if (Array.isArray(input)) {\n return input.map((v) => {\n const r = one(v);\n return typeof r === \"string\" ? { text: r } : r;\n });\n }\n return one(input);\n}\n\n/**\n * Load fixtures from a directory: every `*.json` file is either one fixture\n * or an array of them. An input may be `{ \"file\": \"page.html\", \"label\": … }`\n * to pull text from a sibling file, which keeps large scraped pages out of\n * the JSON.\n */\nexport function loadFixtures(dir: string): EvalFixture[] {\n const root = resolve(dir);\n if (!existsSync(root)) {\n throw new Error(`Fixture directory not found: ${root}`);\n }\n const fixtures: EvalFixture[] = [];\n const files = readdirSync(root)\n .filter((f) => f.endsWith(\".json\") && !f.startsWith(\".\"))\n .sort();\n for (const file of files) {\n const path = join(root, file);\n const parsed = JSON.parse(readFileSync(path, \"utf8\")) as unknown;\n const list = Array.isArray(parsed) ? parsed : [parsed];\n list.forEach((raw, index) => {\n const fixture = raw as EvalFixture;\n if (!fixture || typeof fixture !== \"object\" || !(\"input\" in fixture) || !(\"expected\" in fixture)) {\n throw new Error(`${path}${list.length > 1 ? `[${index}]` : \"\"}: a fixture needs \"input\" and \"expected\"`);\n }\n const stem = basename(file, \".json\");\n fixtures.push({\n name: fixture.name ?? (list.length > 1 ? `${stem}[${index}]` : stem),\n input: resolveInput(fixture.input, dirname(path)),\n expected: fixture.expected,\n });\n });\n }\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Scoring\n\nfunction stable(value: unknown): string {\n return JSON.stringify(value, (_k, v) =>\n v && typeof v === \"object\" && !Array.isArray(v)\n ? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : v,\n );\n}\n\nfunction isPrimitive(value: unknown): boolean {\n return value === null || typeof value !== \"object\";\n}\n\n/**\n * Flatten a value to its leaves, keyed by dotted path. Objects recurse;\n * arrays are leaves. `null` and `undefined` are absence, not leaves.\n */\nexport function flattenLeaves(value: unknown, prefix = \"\"): Map<string, unknown> {\n const leaves = new Map<string, unknown>();\n if (value === null || value === undefined) return leaves;\n if (typeof value !== \"object\" || Array.isArray(value)) {\n leaves.set(prefix, value);\n return leaves;\n }\n for (const [key, child] of Object.entries(value as Record<string, unknown>)) {\n const path = prefix ? `${prefix}.${key}` : key;\n for (const [leafPath, leaf] of flattenLeaves(child, path)) {\n leaves.set(leafPath, leaf);\n }\n }\n return leaves;\n}\n\n/**\n * Whether two leaves agree. Arrays of primitives compare as multisets —\n * the order amenities come back in is not a fact about the listing —\n * and everything else compares structurally.\n */\nexport function leavesEqual(a: unknown, b: unknown): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n if (a.every(isPrimitive) && b.every(isPrimitive)) {\n const sortedA = [...a].map(String).sort();\n const sortedB = [...b].map(String).sort();\n return sortedA.every((v, i) => v === sortedB[i]);\n }\n }\n return stable(a) === stable(b);\n}\n\n/** Compare one extraction against its expectation, leaf by leaf. */\nexport function compareLeaves(\n expected: Record<string, unknown>,\n actual: Record<string, unknown> | undefined,\n provenance: Record<string, FieldProvenance> = {},\n): LeafResult[] {\n const want = flattenLeaves(expected);\n const got = flattenLeaves(actual ?? {});\n const paths = [...new Set([...want.keys(), ...got.keys()])].sort();\n return paths.map((path) => {\n const top = path.split(\".\")[0];\n const confidence = provenance[top]?.confidence;\n const result: LeafResult = { path, outcome: \"match\" };\n if (confidence) result.confidence = confidence;\n if (want.has(path) && got.has(path)) {\n result.expected = want.get(path);\n result.actual = got.get(path);\n result.outcome = leavesEqual(want.get(path), got.get(path)) ? \"match\" : \"wrong\";\n } else if (want.has(path)) {\n result.expected = want.get(path);\n result.outcome = \"missing\";\n } else {\n result.actual = got.get(path);\n result.outcome = \"extra\";\n }\n return result;\n });\n}\n\nfunction ratio(num: number, den: number): number | null {\n return den === 0 ? null : num / den;\n}\n\n/** Aggregate leaf outcomes into per-field precision and recall. */\nexport function fieldStats(items: readonly EvalItem[]): FieldStats[] {\n const byPath = new Map<string, { tp: number; fp: number; fn: number }>();\n for (const item of items) {\n for (const leaf of item.leaves) {\n const stats = byPath.get(leaf.path) ?? { tp: 0, fp: 0, fn: 0 };\n switch (leaf.outcome) {\n case \"match\":\n stats.tp += 1;\n break;\n case \"wrong\":\n stats.fp += 1;\n stats.fn += 1;\n break;\n case \"missing\":\n stats.fn += 1;\n break;\n case \"extra\":\n stats.fp += 1;\n break;\n }\n byPath.set(leaf.path, stats);\n }\n }\n return [...byPath.entries()]\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([path, { tp, fp, fn }]) => ({\n path,\n tp,\n fp,\n fn,\n precision: ratio(tp, tp + fp),\n recall: ratio(tp, tp + fn),\n }));\n}\n\nfunction percentile(sorted: readonly number[], p: number): number {\n if (sorted.length === 0) return 0;\n const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);\n return sorted[Math.max(0, index)];\n}\n\nfunction emptyUsage(): Usage {\n return { promptTokens: 0, completionTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };\n}\n\nfunction addUsage(into: Usage, usage: ProviderUsage | undefined): void {\n if (!usage) return;\n into.promptTokens += usage.promptTokens;\n into.completionTokens += usage.completionTokens;\n into.cacheReadTokens += usage.cacheReadTokens ?? 0;\n into.cacheWriteTokens += usage.cacheWriteTokens ?? 0;\n}\n\n/** Dollars, from per-million-token prices. */\nexport function estimateCost(usage: Usage, prices: TokenPrices): number {\n const per = (tokens: number, price: number) => (tokens / 1_000_000) * price;\n return (\n per(usage.promptTokens, prices.inputPerMTok) +\n per(usage.completionTokens, prices.outputPerMTok) +\n per(usage.cacheReadTokens, prices.cacheReadPerMTok ?? prices.inputPerMTok) +\n per(usage.cacheWriteTokens, prices.cacheWritePerMTok ?? prices.inputPerMTok)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Running\n\ninterface ItemContext {\n usage: Usage;\n calls: number;\n}\n\nconst context = new AsyncLocalStorage<ItemContext>();\n\n/** Credits each call's usage to whichever fixture is running it. */\nclass MeteredProvider implements Provider {\n constructor(private readonly inner: Provider) {}\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const response = await this.inner.complete(request);\n const ctx = context.getStore();\n if (ctx) {\n ctx.calls += 1;\n addUsage(ctx.usage, response.usage);\n }\n return response;\n }\n}\n\nasync function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results: R[] = new Array(items.length);\n let next = 0;\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {\n while (next < items.length) {\n const index = next++;\n results[index] = await fn(items[index], index);\n }\n });\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Run every fixture through a coercion and score the results.\n *\n * Per-field precision and recall make a description change measurable: a\n * field whose recall dropped after a rewording is a field the rewording\n * hurt. Token usage and latency come along so the cost of a change is\n * visible next to its effect. Pair it with a `ReplayProvider` to run in CI\n * without spend.\n */\nexport async function runEval(options: EvalOptions): Promise<EvalReport> {\n const {\n fixtures,\n mode = \"coerce\",\n provenance = false,\n concurrency = 1,\n prices,\n provider,\n ...coerceOptions\n } = options;\n const metered = new MeteredProvider(provider);\n const run = mode === \"coerce\"\n ? provenance ? coerceWithProvenance : coerce\n : provenance ? partialCoerceWithProvenance : partialCoerce;\n\n const items = await mapWithConcurrency(fixtures, concurrency, async (fixture, index) => {\n const ctx: ItemContext = { usage: emptyUsage(), calls: 0 };\n const name = fixture.name ?? `fixture ${index + 1}`;\n const started = performance.now();\n return context.run(ctx, async (): Promise<EvalItem> => {\n try {\n const result = await run<Record<string, unknown>>(fixture.input, {\n ...coerceOptions,\n provider: metered,\n });\n const data = provenance\n ? (result as { data: Record<string, unknown> }).data\n : (result as Record<string, unknown>);\n const prov = provenance ? (result as { provenance: Record<string, FieldProvenance> }).provenance : {};\n const issues = provenance ? (result as { issues: ResolvedIssue[] }).issues : [];\n const leaves = compareLeaves(fixture.expected, data, prov);\n return {\n name,\n ok: true,\n exact: leaves.every((l) => l.outcome === \"match\"),\n leaves,\n issues,\n latencyMs: Math.round(performance.now() - started),\n calls: ctx.calls,\n usage: ctx.usage,\n };\n } catch (error) {\n return {\n name,\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n exact: false,\n leaves: compareLeaves(fixture.expected, undefined),\n issues: [],\n latencyMs: Math.round(performance.now() - started),\n calls: ctx.calls,\n usage: ctx.usage,\n };\n }\n });\n });\n\n const fields = fieldStats(items);\n const tp = fields.reduce((s, f) => s + f.tp, 0);\n const fp = fields.reduce((s, f) => s + f.fp, 0);\n const fn = fields.reduce((s, f) => s + f.fn, 0);\n const usage = emptyUsage();\n for (const item of items) {\n usage.promptTokens += item.usage.promptTokens;\n usage.completionTokens += item.usage.completionTokens;\n usage.cacheReadTokens += item.usage.cacheReadTokens;\n usage.cacheWriteTokens += item.usage.cacheWriteTokens;\n }\n const latencies = items.map((i) => i.latencyMs).sort((a, b) => a - b);\n\n return {\n schemaId: options.schema.id,\n mode,\n ranAt: new Date().toISOString(),\n items,\n fields,\n totals: {\n fixtures: items.length,\n ok: items.filter((i) => i.ok).length,\n exact: items.filter((i) => i.exact).length,\n precision: ratio(tp, tp + fp),\n recall: ratio(tp, tp + fn),\n usage,\n calls: items.reduce((s, i) => s + i.calls, 0),\n ...(prices ? { cost: estimateCost(usage, prices) } : {}),\n latencyMs: {\n p50: percentile(latencies, 50),\n p95: percentile(latencies, 95),\n max: latencies[latencies.length - 1] ?? 0,\n total: latencies.reduce((s, l) => s + l, 0),\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Persistence and diffs\n\nexport function saveReport(report: EvalReport, file: string): void {\n mkdirSync(dirname(resolve(file)), { recursive: true });\n writeFileSync(resolve(file), JSON.stringify(report, null, 2) + \"\\n\");\n}\n\nexport function loadReport(file: string): EvalReport | undefined {\n const path = resolve(file);\n if (!existsSync(path)) return undefined;\n return JSON.parse(readFileSync(path, \"utf8\")) as EvalReport;\n}\n\nexport interface FieldDelta {\n path: string;\n precision: number | null;\n recall: number | null;\n}\n\n/** What changed between two runs. Deltas are `next − previous`. */\nexport interface EvalDiff {\n exact: number;\n ok: number;\n precision: number | null;\n recall: number | null;\n promptTokens: number;\n completionTokens: number;\n cost?: number;\n p50Ms: number;\n /** Fields whose precision or recall moved, largest drop first. */\n fields: FieldDelta[];\n}\n\nfunction delta(a: number | null | undefined, b: number | null | undefined): number | null {\n return a === null || a === undefined || b === null || b === undefined ? null : b - a;\n}\n\nexport function diffReports(previous: EvalReport, next: EvalReport): EvalDiff {\n const prevFields = new Map(previous.fields.map((f) => [f.path, f]));\n const fields: FieldDelta[] = [];\n for (const field of next.fields) {\n const before = prevFields.get(field.path);\n const precision = delta(before?.precision, field.precision);\n const recall = delta(before?.recall, field.recall);\n if ((precision !== null && precision !== 0) || (recall !== null && recall !== 0)) {\n fields.push({ path: field.path, precision, recall });\n }\n }\n fields.sort((a, b) => Math.min(a.precision ?? 0, a.recall ?? 0) - Math.min(b.precision ?? 0, b.recall ?? 0));\n\n return {\n exact: next.totals.exact - previous.totals.exact,\n ok: next.totals.ok - previous.totals.ok,\n precision: delta(previous.totals.precision, next.totals.precision),\n recall: delta(previous.totals.recall, next.totals.recall),\n promptTokens: next.totals.usage.promptTokens - previous.totals.usage.promptTokens,\n completionTokens: next.totals.usage.completionTokens - previous.totals.usage.completionTokens,\n ...(next.totals.cost !== undefined && previous.totals.cost !== undefined\n ? { cost: next.totals.cost - previous.totals.cost }\n : {}),\n p50Ms: next.totals.latencyMs.p50 - previous.totals.latencyMs.p50,\n fields,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Formatting\n\nfunction pct(value: number | null): string {\n return value === null ? \" – \" : `${(value * 100).toFixed(0).padStart(3)}%`;\n}\n\nfunction signed(value: number | null, digits = 0, suffix = \"\"): string {\n if (value === null || value === 0) return \"\";\n const text = digits > 0 ? value.toFixed(digits) : String(value);\n return ` (${value > 0 ? \"+\" : \"\"}${text}${suffix})`;\n}\n\nfunction signedPct(value: number | null): string {\n return value === null || value === 0 ? \"\" : ` (${value > 0 ? \"+\" : \"\"}${(value * 100).toFixed(0)}pt)`;\n}\n\n/** A plain-text report for a terminal, with deltas when a previous run is given. */\nexport function formatReport(report: EvalReport, diff?: EvalDiff): string {\n const t = report.totals;\n const lines: string[] = [];\n lines.push(\n `Eval: ${report.schemaId} (${report.mode}) — ${t.fixtures} fixture(s), ${t.ok} ran${signed(diff?.ok ?? null)}, ${t.exact} exact${signed(diff?.exact ?? null)}`,\n );\n lines.push(\"\");\n\n const width = Math.max(5, ...report.fields.map((f) => f.path.length));\n const deltas = new Map((diff?.fields ?? []).map((f) => [f.path, f]));\n lines.push(`${\"Field\".padEnd(width)} Prec Recall tp fp fn Δ`);\n const col = (value: number | null, w: number) => pct(value).trim().padStart(w);\n for (const field of report.fields) {\n const d = deltas.get(field.path);\n const change = d\n ? [d.precision !== null && d.precision !== 0 ? `P${signedPct(d.precision).trim()}` : \"\", d.recall !== null && d.recall !== 0 ? `R${signedPct(d.recall).trim()}` : \"\"]\n .filter(Boolean)\n .join(\" \")\n : \"\";\n lines.push(\n `${field.path.padEnd(width)} ${col(field.precision, 4)} ${col(field.recall, 6)} ${String(field.tp).padStart(2)} ${String(field.fp).padStart(2)} ${String(field.fn).padStart(2)} ${change}`,\n );\n }\n lines.push(\"\");\n lines.push(\n `Overall: precision ${pct(t.precision).trim()}${signedPct(diff?.precision ?? null)}, recall ${pct(t.recall).trim()}${signedPct(diff?.recall ?? null)}`,\n );\n const u = t.usage;\n const cache = u.cacheReadTokens || u.cacheWriteTokens\n ? `, cache read ${u.cacheReadTokens.toLocaleString(\"en-US\")} / write ${u.cacheWriteTokens.toLocaleString(\"en-US\")}`\n : \"\";\n lines.push(\n `Tokens: ${u.promptTokens.toLocaleString(\"en-US\")} prompt${signed(diff?.promptTokens ?? null)} / ${u.completionTokens.toLocaleString(\"en-US\")} completion${signed(diff?.completionTokens ?? null)}${cache}; ${t.calls} call(s)` +\n (t.cost !== undefined ? `; cost $${t.cost.toFixed(4)}${signed(diff?.cost ?? null, 4)}` : \"\"),\n );\n lines.push(\n `Latency: p50 ${t.latencyMs.p50}ms${signed(diff?.p50Ms ?? null, 0, \"ms\")}, p95 ${t.latencyMs.p95}ms, max ${t.latencyMs.max}ms`,\n );\n\n const failed = report.items.filter((i) => !i.ok);\n if (failed.length > 0) {\n lines.push(\"\");\n lines.push(\"Failed:\");\n for (const item of failed) lines.push(` ${item.name}: ${item.error}`);\n }\n const imperfect = report.items.filter((i) => i.ok && !i.exact);\n if (imperfect.length > 0) {\n lines.push(\"\");\n lines.push(\"Mismatches:\");\n for (const item of imperfect) {\n for (const leaf of item.leaves.filter((l) => l.outcome !== \"match\")) {\n const detail =\n leaf.outcome === \"wrong\"\n ? `expected ${JSON.stringify(leaf.expected)}, got ${JSON.stringify(leaf.actual)}`\n : leaf.outcome === \"missing\"\n ? `expected ${JSON.stringify(leaf.expected)}, got nothing`\n : `unexpected ${JSON.stringify(leaf.actual)}`;\n const conf = leaf.confidence ? ` [${leaf.confidence}]` : \"\";\n lines.push(` ${item.name} › ${leaf.path}: ${detail}${conf}`);\n }\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":["readdirSync","readFileSync","existsSync","mkdirSync","writeFileSync","join","stable"]}
|
|
1
|
+
{"version":3,"sources":["../src/replay.ts","../src/eval.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Provider, ProviderRequest, ProviderResponse } from \"@sembl/core\";\n\n/** What a recording holds: enough of the request to recognise it, and the answer. */\nexport interface Recording {\n /** Hash of the parts of the request that determine the answer. */\n key: string;\n schemaId: string;\n request: {\n systemPrompt: string;\n userInput: string;\n jsonSchema: Record<string, unknown>;\n history?: ProviderRequest[\"history\"];\n };\n response: ProviderResponse;\n recordedAt: string;\n}\n\n/** JSON with sorted keys, so the same request always hashes the same. */\nfunction stable(value: unknown): string {\n return JSON.stringify(value, (_key, v) =>\n v && typeof v === \"object\" && !Array.isArray(v)\n ? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : v,\n );\n}\n\n/**\n * The key a request records under. Only the parts that reach the model\n * count: the system prompt, the user input and the JSON Schema. The runtime\n * schema and bundle are already folded into those.\n */\nexport function recordingKey(request: ProviderRequest): string {\n const material = stable({\n systemPrompt: request.systemPrompt,\n userInput: request.userInput,\n jsonSchema: request.jsonSchema,\n // A repair call shares everything above with the call it repairs.\n ...(request.history && request.history.length > 0 ? { history: request.history } : {}),\n });\n return createHash(\"sha256\").update(material).digest(\"hex\").slice(0, 24);\n}\n\nfunction slug(id: string): string {\n return id.replace(/[^A-Za-z0-9_-]+/g, \"_\").slice(0, 40) || \"schema\";\n}\n\n/** Where a recording for a request lives inside a directory. */\nexport function recordingPath(dir: string, request: ProviderRequest): string {\n return join(dir, `${slug(request.schema.id)}.${recordingKey(request)}.json`);\n}\n\n/** Thrown by {@link ReplayProvider} when no recording matches a request. */\nexport class ReplayMissError extends Error {\n constructor(\n public readonly key: string,\n public readonly dir: string,\n public readonly schemaId: string,\n ) {\n super(\n `No recording for a \"${schemaId}\" request (key ${key}) in ${dir}. ` +\n \"Run once with a RecordingProvider, or pass a fallback provider to record misses.\",\n );\n this.name = \"ReplayMissError\";\n }\n}\n\n/**\n * Calls another provider and writes every request/response pair to a\n * directory, one JSON file each, named by schema and request hash. Run your\n * extraction code through it once with real credentials; the files it leaves\n * behind let a {@link ReplayProvider} answer the same requests offline.\n *\n * A request is identified by what reaches the model, so editing a field\n * description or the input produces a new recording rather than a stale hit.\n */\nexport class RecordingProvider implements Provider {\n readonly supportsHistory: boolean;\n\n constructor(\n private readonly inner: Provider,\n private readonly dir: string,\n ) {\n this.supportsHistory = inner.supportsHistory === true;\n mkdirSync(dir, { recursive: true });\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const response = await this.inner.complete(request);\n const recording: Recording = {\n key: recordingKey(request),\n schemaId: request.schema.id,\n request: {\n systemPrompt: request.systemPrompt,\n userInput: request.userInput,\n jsonSchema: request.jsonSchema,\n ...(request.history ? { history: request.history } : {}),\n },\n response,\n recordedAt: new Date().toISOString(),\n };\n writeFileSync(recordingPath(this.dir, request), JSON.stringify(recording, null, 2) + \"\\n\");\n return response;\n }\n}\n\n/** Options for {@link ReplayProvider}. */\nexport interface ReplayOptions {\n /**\n * Where to send a request no recording matches. The answer is recorded,\n * so the next run replays it. Without one, a miss throws\n * {@link ReplayMissError} — the right behaviour in CI, where a miss means\n * a fixture changed and nobody re-recorded.\n */\n fallback?: Provider;\n}\n\n/**\n * Answers requests from a directory of recordings, never touching the\n * network. Deterministic, free, and fast: the provider to put under tests of\n * your own extraction code.\n */\nexport class ReplayProvider implements Provider {\n private readonly recorder: RecordingProvider | undefined;\n /**\n * Follows the fallback when there is one. A strict replay claims support\n * so that a recorded multi-turn repair is asked for the same way it was\n * recorded; the recordings decide whether it is answered.\n */\n readonly supportsHistory: boolean;\n\n constructor(\n private readonly dir: string,\n options: ReplayOptions = {},\n ) {\n this.recorder = options.fallback ? new RecordingProvider(options.fallback, dir) : undefined;\n this.supportsHistory = options.fallback ? options.fallback.supportsHistory === true : true;\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const path = recordingPath(this.dir, request);\n if (existsSync(path)) {\n const recording = JSON.parse(readFileSync(path, \"utf8\")) as Recording;\n return recording.response;\n }\n if (this.recorder) {\n return this.recorder.complete(request);\n }\n throw new ReplayMissError(recordingKey(request), this.dir, request.schema.id);\n }\n\n /** How many recordings the directory holds. */\n size(): number {\n if (!existsSync(this.dir)) return 0;\n return readdirSync(this.dir).filter((f) => f.endsWith(\".json\")).length;\n }\n}\n\n/**\n * The usual arrangement: replay from `dir`, and record misses through\n * `live` when it is given — say, only when an API key is present — so the\n * same test file works locally with credentials and in CI without them.\n */\nexport function replayOrRecord(dir: string, live?: Provider): Provider {\n return new ReplayProvider(dir, live ? { fallback: live } : {});\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport {\n coerce,\n partialCoerce,\n coerceWithProvenance,\n partialCoerceWithProvenance,\n} from \"@sembl/core\";\nimport type {\n CoerceInput,\n CoerceOptions,\n FieldProvenance,\n Provider,\n ProviderRequest,\n ProviderResponse,\n ProviderUsage,\n ResolvedIssue,\n Source,\n} from \"@sembl/core\";\n\n/** One case: an input and what a correct extraction of it looks like. */\nexport interface EvalFixture {\n /** Shown in the report. Defaults to the file it was loaded from. */\n name?: string;\n input: CoerceInput;\n /** The expected extraction. Absent and `null` both mean \"not present\". */\n expected: Record<string, unknown>;\n}\n\n/** Per-million-token prices, for a cost line in the report. */\nexport interface TokenPrices {\n inputPerMTok: number;\n outputPerMTok: number;\n /** Defaults to `inputPerMTok` when the provider reports cache reads. */\n cacheReadPerMTok?: number;\n /** Defaults to `inputPerMTok` when the provider reports cache writes. */\n cacheWritePerMTok?: number;\n}\n\n/** Options for {@link runEval}. Everything in `CoerceOptions` applies per fixture. */\nexport interface EvalOptions extends CoerceOptions {\n fixtures: readonly EvalFixture[];\n /** Which coercion to run. Default `\"coerce\"`. */\n mode?: \"coerce\" | \"partialCoerce\";\n /** Ask for provenance, so the report can show confidence per field. */\n provenance?: boolean;\n /** How many fixtures run at once. Default 1, for stable latency numbers. */\n concurrency?: number;\n /** Prices for the cost line. Without them the report has no cost. */\n prices?: TokenPrices;\n}\n\n/** How one leaf compared. */\nexport type LeafOutcome = \"match\" | \"wrong\" | \"missing\" | \"extra\";\n\nexport interface LeafResult {\n path: string;\n outcome: LeafOutcome;\n expected?: unknown;\n actual?: unknown;\n confidence?: FieldProvenance[\"confidence\"];\n}\n\nexport interface EvalItem {\n name: string;\n ok: boolean;\n /** Message of the error the coercion threw, when it did. */\n error?: string;\n /** Whether every expected leaf matched and nothing extra came back. */\n exact: boolean;\n leaves: LeafResult[];\n issues: ResolvedIssue[];\n latencyMs: number;\n calls: number;\n usage: Usage;\n}\n\nexport interface Usage {\n promptTokens: number;\n completionTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n}\n\nexport interface FieldStats {\n path: string;\n tp: number;\n fp: number;\n fn: number;\n /** `tp / (tp + fp)`; null when nothing was returned for the field. */\n precision: number | null;\n /** `tp / (tp + fn)`; null when nothing was expected for the field. */\n recall: number | null;\n}\n\nexport interface EvalTotals {\n fixtures: number;\n ok: number;\n exact: number;\n precision: number | null;\n recall: number | null;\n usage: Usage;\n calls: number;\n cost?: number;\n latencyMs: { p50: number; p95: number; max: number; total: number };\n}\n\nexport interface EvalReport {\n schemaId: string;\n mode: \"coerce\" | \"partialCoerce\";\n ranAt: string;\n items: EvalItem[];\n fields: FieldStats[];\n totals: EvalTotals;\n}\n\n// ---------------------------------------------------------------------------\n// Fixtures\n\ninterface FileInput {\n file: string;\n label?: string;\n}\n\nfunction isFileInput(value: unknown): value is FileInput {\n return typeof value === \"object\" && value !== null && typeof (value as FileInput).file === \"string\";\n}\n\n/** Resolve `{ file, label }` inputs against the fixture's own directory. */\nfunction resolveInput(input: unknown, baseDir: string): CoerceInput {\n const one = (value: unknown): Source | string => {\n if (typeof value === \"string\") return value;\n if (isFileInput(value)) {\n const text = readFileSync(resolve(baseDir, value.file), \"utf8\");\n return value.label ? { label: value.label, text } : { text };\n }\n return value as Source;\n };\n if (Array.isArray(input)) {\n return input.map((v) => {\n const r = one(v);\n return typeof r === \"string\" ? { text: r } : r;\n });\n }\n return one(input);\n}\n\n/**\n * Load fixtures from a directory: every `*.json` file is either one fixture\n * or an array of them. An input may be `{ \"file\": \"page.html\", \"label\": … }`\n * to pull text from a sibling file, which keeps large scraped pages out of\n * the JSON.\n */\nexport function loadFixtures(dir: string): EvalFixture[] {\n const root = resolve(dir);\n if (!existsSync(root)) {\n throw new Error(`Fixture directory not found: ${root}`);\n }\n const fixtures: EvalFixture[] = [];\n const files = readdirSync(root)\n .filter((f) => f.endsWith(\".json\") && !f.startsWith(\".\"))\n .sort();\n for (const file of files) {\n const path = join(root, file);\n const parsed = JSON.parse(readFileSync(path, \"utf8\")) as unknown;\n const list = Array.isArray(parsed) ? parsed : [parsed];\n list.forEach((raw, index) => {\n const fixture = raw as EvalFixture;\n if (!fixture || typeof fixture !== \"object\" || !(\"input\" in fixture) || !(\"expected\" in fixture)) {\n throw new Error(`${path}${list.length > 1 ? `[${index}]` : \"\"}: a fixture needs \"input\" and \"expected\"`);\n }\n const stem = basename(file, \".json\");\n fixtures.push({\n name: fixture.name ?? (list.length > 1 ? `${stem}[${index}]` : stem),\n input: resolveInput(fixture.input, dirname(path)),\n expected: fixture.expected,\n });\n });\n }\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Scoring\n\nfunction stable(value: unknown): string {\n return JSON.stringify(value, (_k, v) =>\n v && typeof v === \"object\" && !Array.isArray(v)\n ? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : v,\n );\n}\n\nfunction isPrimitive(value: unknown): boolean {\n return value === null || typeof value !== \"object\";\n}\n\n/**\n * Flatten a value to its leaves, keyed by dotted path. Objects recurse;\n * arrays are leaves. `null` and `undefined` are absence, not leaves.\n */\nexport function flattenLeaves(value: unknown, prefix = \"\"): Map<string, unknown> {\n const leaves = new Map<string, unknown>();\n if (value === null || value === undefined) return leaves;\n if (typeof value !== \"object\" || Array.isArray(value)) {\n leaves.set(prefix, value);\n return leaves;\n }\n for (const [key, child] of Object.entries(value as Record<string, unknown>)) {\n const path = prefix ? `${prefix}.${key}` : key;\n for (const [leafPath, leaf] of flattenLeaves(child, path)) {\n leaves.set(leafPath, leaf);\n }\n }\n return leaves;\n}\n\n/**\n * Whether two leaves agree. Arrays of primitives compare as multisets —\n * the order amenities come back in is not a fact about the listing —\n * and everything else compares structurally.\n */\nexport function leavesEqual(a: unknown, b: unknown): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n if (a.every(isPrimitive) && b.every(isPrimitive)) {\n const sortedA = [...a].map(String).sort();\n const sortedB = [...b].map(String).sort();\n return sortedA.every((v, i) => v === sortedB[i]);\n }\n }\n return stable(a) === stable(b);\n}\n\n/** Compare one extraction against its expectation, leaf by leaf. */\nexport function compareLeaves(\n expected: Record<string, unknown>,\n actual: Record<string, unknown> | undefined,\n provenance: Record<string, FieldProvenance> = {},\n): LeafResult[] {\n const want = flattenLeaves(expected);\n const got = flattenLeaves(actual ?? {});\n const paths = [...new Set([...want.keys(), ...got.keys()])].sort();\n return paths.map((path) => {\n const top = path.split(\".\")[0];\n const confidence = provenance[top]?.confidence;\n const result: LeafResult = { path, outcome: \"match\" };\n if (confidence) result.confidence = confidence;\n if (want.has(path) && got.has(path)) {\n result.expected = want.get(path);\n result.actual = got.get(path);\n result.outcome = leavesEqual(want.get(path), got.get(path)) ? \"match\" : \"wrong\";\n } else if (want.has(path)) {\n result.expected = want.get(path);\n result.outcome = \"missing\";\n } else {\n result.actual = got.get(path);\n result.outcome = \"extra\";\n }\n return result;\n });\n}\n\nfunction ratio(num: number, den: number): number | null {\n return den === 0 ? null : num / den;\n}\n\n/** Aggregate leaf outcomes into per-field precision and recall. */\nexport function fieldStats(items: readonly EvalItem[]): FieldStats[] {\n const byPath = new Map<string, { tp: number; fp: number; fn: number }>();\n for (const item of items) {\n for (const leaf of item.leaves) {\n const stats = byPath.get(leaf.path) ?? { tp: 0, fp: 0, fn: 0 };\n switch (leaf.outcome) {\n case \"match\":\n stats.tp += 1;\n break;\n case \"wrong\":\n stats.fp += 1;\n stats.fn += 1;\n break;\n case \"missing\":\n stats.fn += 1;\n break;\n case \"extra\":\n stats.fp += 1;\n break;\n }\n byPath.set(leaf.path, stats);\n }\n }\n return [...byPath.entries()]\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([path, { tp, fp, fn }]) => ({\n path,\n tp,\n fp,\n fn,\n precision: ratio(tp, tp + fp),\n recall: ratio(tp, tp + fn),\n }));\n}\n\nfunction percentile(sorted: readonly number[], p: number): number {\n if (sorted.length === 0) return 0;\n const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);\n return sorted[Math.max(0, index)];\n}\n\nfunction emptyUsage(): Usage {\n return { promptTokens: 0, completionTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };\n}\n\nfunction addUsage(into: Usage, usage: ProviderUsage | undefined): void {\n if (!usage) return;\n into.promptTokens += usage.promptTokens;\n into.completionTokens += usage.completionTokens;\n into.cacheReadTokens += usage.cacheReadTokens ?? 0;\n into.cacheWriteTokens += usage.cacheWriteTokens ?? 0;\n}\n\n/** Dollars, from per-million-token prices. */\nexport function estimateCost(usage: Usage, prices: TokenPrices): number {\n const per = (tokens: number, price: number) => (tokens / 1_000_000) * price;\n return (\n per(usage.promptTokens, prices.inputPerMTok) +\n per(usage.completionTokens, prices.outputPerMTok) +\n per(usage.cacheReadTokens, prices.cacheReadPerMTok ?? prices.inputPerMTok) +\n per(usage.cacheWriteTokens, prices.cacheWritePerMTok ?? prices.inputPerMTok)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Running\n\ninterface ItemContext {\n usage: Usage;\n calls: number;\n}\n\nconst context = new AsyncLocalStorage<ItemContext>();\n\n/** Credits each call's usage to whichever fixture is running it. */\nclass MeteredProvider implements Provider {\n readonly supportsHistory: boolean;\n\n constructor(private readonly inner: Provider) {\n this.supportsHistory = inner.supportsHistory === true;\n }\n\n async complete(request: ProviderRequest): Promise<ProviderResponse> {\n const response = await this.inner.complete(request);\n const ctx = context.getStore();\n if (ctx) {\n ctx.calls += 1;\n addUsage(ctx.usage, response.usage);\n }\n return response;\n }\n}\n\nasync function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results: R[] = new Array(items.length);\n let next = 0;\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {\n while (next < items.length) {\n const index = next++;\n results[index] = await fn(items[index], index);\n }\n });\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Run every fixture through a coercion and score the results.\n *\n * Per-field precision and recall make a description change measurable: a\n * field whose recall dropped after a rewording is a field the rewording\n * hurt. Token usage and latency come along so the cost of a change is\n * visible next to its effect. Pair it with a `ReplayProvider` to run in CI\n * without spend.\n */\nexport async function runEval(options: EvalOptions): Promise<EvalReport> {\n const {\n fixtures,\n mode = \"coerce\",\n provenance = false,\n concurrency = 1,\n prices,\n provider,\n ...coerceOptions\n } = options;\n const metered = new MeteredProvider(provider);\n const run = mode === \"coerce\"\n ? provenance ? coerceWithProvenance : coerce\n : provenance ? partialCoerceWithProvenance : partialCoerce;\n\n const items = await mapWithConcurrency(fixtures, concurrency, async (fixture, index) => {\n const ctx: ItemContext = { usage: emptyUsage(), calls: 0 };\n const name = fixture.name ?? `fixture ${index + 1}`;\n const started = performance.now();\n return context.run(ctx, async (): Promise<EvalItem> => {\n try {\n const result = await run<Record<string, unknown>>(fixture.input, {\n ...coerceOptions,\n provider: metered,\n });\n const data = provenance\n ? (result as { data: Record<string, unknown> }).data\n : (result as Record<string, unknown>);\n const prov = provenance ? (result as { provenance: Record<string, FieldProvenance> }).provenance : {};\n const issues = provenance ? (result as { issues: ResolvedIssue[] }).issues : [];\n const leaves = compareLeaves(fixture.expected, data, prov);\n return {\n name,\n ok: true,\n exact: leaves.every((l) => l.outcome === \"match\"),\n leaves,\n issues,\n latencyMs: Math.round(performance.now() - started),\n calls: ctx.calls,\n usage: ctx.usage,\n };\n } catch (error) {\n return {\n name,\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n exact: false,\n leaves: compareLeaves(fixture.expected, undefined),\n issues: [],\n latencyMs: Math.round(performance.now() - started),\n calls: ctx.calls,\n usage: ctx.usage,\n };\n }\n });\n });\n\n const fields = fieldStats(items);\n const tp = fields.reduce((s, f) => s + f.tp, 0);\n const fp = fields.reduce((s, f) => s + f.fp, 0);\n const fn = fields.reduce((s, f) => s + f.fn, 0);\n const usage = emptyUsage();\n for (const item of items) {\n usage.promptTokens += item.usage.promptTokens;\n usage.completionTokens += item.usage.completionTokens;\n usage.cacheReadTokens += item.usage.cacheReadTokens;\n usage.cacheWriteTokens += item.usage.cacheWriteTokens;\n }\n const latencies = items.map((i) => i.latencyMs).sort((a, b) => a - b);\n\n return {\n schemaId: options.schema.id,\n mode,\n ranAt: new Date().toISOString(),\n items,\n fields,\n totals: {\n fixtures: items.length,\n ok: items.filter((i) => i.ok).length,\n exact: items.filter((i) => i.exact).length,\n precision: ratio(tp, tp + fp),\n recall: ratio(tp, tp + fn),\n usage,\n calls: items.reduce((s, i) => s + i.calls, 0),\n ...(prices ? { cost: estimateCost(usage, prices) } : {}),\n latencyMs: {\n p50: percentile(latencies, 50),\n p95: percentile(latencies, 95),\n max: latencies[latencies.length - 1] ?? 0,\n total: latencies.reduce((s, l) => s + l, 0),\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Persistence and diffs\n\nexport function saveReport(report: EvalReport, file: string): void {\n mkdirSync(dirname(resolve(file)), { recursive: true });\n writeFileSync(resolve(file), JSON.stringify(report, null, 2) + \"\\n\");\n}\n\nexport function loadReport(file: string): EvalReport | undefined {\n const path = resolve(file);\n if (!existsSync(path)) return undefined;\n return JSON.parse(readFileSync(path, \"utf8\")) as EvalReport;\n}\n\nexport interface FieldDelta {\n path: string;\n precision: number | null;\n recall: number | null;\n}\n\n/** What changed between two runs. Deltas are `next − previous`. */\nexport interface EvalDiff {\n exact: number;\n ok: number;\n precision: number | null;\n recall: number | null;\n promptTokens: number;\n completionTokens: number;\n cost?: number;\n p50Ms: number;\n /** Fields whose precision or recall moved, largest drop first. */\n fields: FieldDelta[];\n}\n\nfunction delta(a: number | null | undefined, b: number | null | undefined): number | null {\n return a === null || a === undefined || b === null || b === undefined ? null : b - a;\n}\n\nexport function diffReports(previous: EvalReport, next: EvalReport): EvalDiff {\n const prevFields = new Map(previous.fields.map((f) => [f.path, f]));\n const fields: FieldDelta[] = [];\n for (const field of next.fields) {\n const before = prevFields.get(field.path);\n const precision = delta(before?.precision, field.precision);\n const recall = delta(before?.recall, field.recall);\n if ((precision !== null && precision !== 0) || (recall !== null && recall !== 0)) {\n fields.push({ path: field.path, precision, recall });\n }\n }\n fields.sort((a, b) => Math.min(a.precision ?? 0, a.recall ?? 0) - Math.min(b.precision ?? 0, b.recall ?? 0));\n\n return {\n exact: next.totals.exact - previous.totals.exact,\n ok: next.totals.ok - previous.totals.ok,\n precision: delta(previous.totals.precision, next.totals.precision),\n recall: delta(previous.totals.recall, next.totals.recall),\n promptTokens: next.totals.usage.promptTokens - previous.totals.usage.promptTokens,\n completionTokens: next.totals.usage.completionTokens - previous.totals.usage.completionTokens,\n ...(next.totals.cost !== undefined && previous.totals.cost !== undefined\n ? { cost: next.totals.cost - previous.totals.cost }\n : {}),\n p50Ms: next.totals.latencyMs.p50 - previous.totals.latencyMs.p50,\n fields,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Formatting\n\nfunction pct(value: number | null): string {\n return value === null ? \" – \" : `${(value * 100).toFixed(0).padStart(3)}%`;\n}\n\nfunction signed(value: number | null, digits = 0, suffix = \"\"): string {\n if (value === null || value === 0) return \"\";\n const text = digits > 0 ? value.toFixed(digits) : String(value);\n return ` (${value > 0 ? \"+\" : \"\"}${text}${suffix})`;\n}\n\nfunction signedPct(value: number | null): string {\n return value === null || value === 0 ? \"\" : ` (${value > 0 ? \"+\" : \"\"}${(value * 100).toFixed(0)}pt)`;\n}\n\n/** A plain-text report for a terminal, with deltas when a previous run is given. */\nexport function formatReport(report: EvalReport, diff?: EvalDiff): string {\n const t = report.totals;\n const lines: string[] = [];\n lines.push(\n `Eval: ${report.schemaId} (${report.mode}) — ${t.fixtures} fixture(s), ${t.ok} ran${signed(diff?.ok ?? null)}, ${t.exact} exact${signed(diff?.exact ?? null)}`,\n );\n lines.push(\"\");\n\n const width = Math.max(5, ...report.fields.map((f) => f.path.length));\n const deltas = new Map((diff?.fields ?? []).map((f) => [f.path, f]));\n lines.push(`${\"Field\".padEnd(width)} Prec Recall tp fp fn Δ`);\n const col = (value: number | null, w: number) => pct(value).trim().padStart(w);\n for (const field of report.fields) {\n const d = deltas.get(field.path);\n const change = d\n ? [d.precision !== null && d.precision !== 0 ? `P${signedPct(d.precision).trim()}` : \"\", d.recall !== null && d.recall !== 0 ? `R${signedPct(d.recall).trim()}` : \"\"]\n .filter(Boolean)\n .join(\" \")\n : \"\";\n lines.push(\n `${field.path.padEnd(width)} ${col(field.precision, 4)} ${col(field.recall, 6)} ${String(field.tp).padStart(2)} ${String(field.fp).padStart(2)} ${String(field.fn).padStart(2)} ${change}`,\n );\n }\n lines.push(\"\");\n lines.push(\n `Overall: precision ${pct(t.precision).trim()}${signedPct(diff?.precision ?? null)}, recall ${pct(t.recall).trim()}${signedPct(diff?.recall ?? null)}`,\n );\n const u = t.usage;\n const cache = u.cacheReadTokens || u.cacheWriteTokens\n ? `, cache read ${u.cacheReadTokens.toLocaleString(\"en-US\")} / write ${u.cacheWriteTokens.toLocaleString(\"en-US\")}`\n : \"\";\n lines.push(\n `Tokens: ${u.promptTokens.toLocaleString(\"en-US\")} prompt${signed(diff?.promptTokens ?? null)} / ${u.completionTokens.toLocaleString(\"en-US\")} completion${signed(diff?.completionTokens ?? null)}${cache}; ${t.calls} call(s)` +\n (t.cost !== undefined ? `; cost $${t.cost.toFixed(4)}${signed(diff?.cost ?? null, 4)}` : \"\"),\n );\n lines.push(\n `Latency: p50 ${t.latencyMs.p50}ms${signed(diff?.p50Ms ?? null, 0, \"ms\")}, p95 ${t.latencyMs.p95}ms, max ${t.latencyMs.max}ms`,\n );\n\n const failed = report.items.filter((i) => !i.ok);\n if (failed.length > 0) {\n lines.push(\"\");\n lines.push(\"Failed:\");\n for (const item of failed) lines.push(` ${item.name}: ${item.error}`);\n }\n const imperfect = report.items.filter((i) => i.ok && !i.exact);\n if (imperfect.length > 0) {\n lines.push(\"\");\n lines.push(\"Mismatches:\");\n for (const item of imperfect) {\n for (const leaf of item.leaves.filter((l) => l.outcome !== \"match\")) {\n const detail =\n leaf.outcome === \"wrong\"\n ? `expected ${JSON.stringify(leaf.expected)}, got ${JSON.stringify(leaf.actual)}`\n : leaf.outcome === \"missing\"\n ? `expected ${JSON.stringify(leaf.expected)}, got nothing`\n : `unexpected ${JSON.stringify(leaf.actual)}`;\n const conf = leaf.confidence ? ` [${leaf.confidence}]` : \"\";\n lines.push(` ${item.name} › ${leaf.path}: ${detail}${conf}`);\n }\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":["readdirSync","readFileSync","existsSync","mkdirSync","writeFileSync","join","stable"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sembl/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Record/replay providers and an eval harness for SEMBL: deterministic tests and measurable prompt changes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"provenance": true
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@sembl/core": "0.
|
|
56
|
+
"@sembl/core": "0.5.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^25.5.0",
|