@theokit/sdk 4.4.2 → 4.5.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e39cdf6: docs: ship the reference docs inside the npm package. `harness-capability-map.md` (every public primitive + its import path) and `error-codes.md` (the `AgentRunError.code` table) are now readable offline at `node_modules/@theokit/sdk/docs/`, pinned to the installed version — useful for agents that read their own dependencies, and for air-gapped setups. They live at the repo root (linked from the root README/CONTRIBUTING/CLAUDE.md) so `build` copies them into the package via `scripts/copy-docs.mjs`, rewriting repo-relative links to absolute GitHub URLs so they still resolve from `node_modules`. A `tests/lint/shipped-docs.test.ts` gate fails if `files` drops the `docs` entry or a new root reference doc is not added to the ship list. Tarball grows ~7 KB. The package README now also points agents at the docs site's machine-readable corpora (`llms.txt` / `llms-full.txt`).
8
+
9
+ ## 4.5.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 283dca0: feat(eval): eval-as-CI-test primitives (SE41). Adds the pieces that turn `@theokit/sdk/eval` into a regression gate you can drop into a pipeline:
14
+
15
+ - **`assertEval(run, thresholds)`** — a pure gate over an `EvalRun` that throws `EvalThresholdError` (carrying the full list of unmet thresholds) when a run misses `minMeanScore`, `minPassRatio`, `maxErrorRatio`, or any `perScorer` floor. Passing returns `void`, so it drops straight into a Vitest `it(...)` or a standalone eval script whose non-zero exit fails CI.
16
+ - **Three new scorers** — `Scorers.levenshtein()` (normalized edit-distance similarity, deterministic), `Scorers.numericDiff()` (relative numeric closeness, deterministic), and `Scorers.embeddingSimilarity()` (cosine of output vs expected embeddings via OpenRouter, or an injected `embed` for other providers/tests). The two deterministic scorers always run in CI with zero token spend.
17
+ - **`EvalOptions.trials`** — repeat each dataset row N times and collapse to one row whose per-scorer score is the mean over the trials (an errored trial contributes 0), smoothing single-model non-determinism. `EvalRowResult.trialCount` records the collapse.
18
+ - A `pnpm eval` script + an OpenRouter-gated `eval` CI workflow run the new `tests/eval/suites/**` eval suites; the deterministic gate also runs on every `pnpm test`.
19
+
20
+ ### Patch Changes
21
+
22
+ - 8932068: fix(build): emit `@theokit/sdk/interactive` CJS type declarations (`dist/interactive/index.d.cts`). The subpath was added to `tsconfig.tools-dts.json` (so `.d.ts` shipped) but omitted from `scripts/mirror-dts-to-cts.mjs`, so `exports["./interactive"].require.types` pointed at a file that was never generated — `publint` and `arethetypeswrong` both flagged it ("No types" from CJS). A CJS `require("@theokit/sdk/interactive")` now resolves its types. Added `dist/interactive` to the mirror target list with a note about the drift trap.
23
+
3
24
  ## 4.2.10
4
25
 
5
26
  ### Patch Changes
package/README.md CHANGED
@@ -24,11 +24,21 @@
24
24
 
25
25
  > **Public beta.** APIs may change before general availability.
26
26
 
27
- For the full reference, see the [root README](../../README.md) and the [**Harness Capability Map**](../../docs/harness-capability-map.md). The exported TypeScript types are the canonical contract.
27
+ For the full reference, see the [root README](https://github.com/usetheodev/theokit-sdk#readme) and the [**Harness Capability Map**](https://github.com/usetheodev/theokit-sdk/blob/main/docs/harness-capability-map.md). The exported TypeScript types are the canonical contract.
28
28
 
29
29
  ## Capability map
30
30
 
31
- New here? The [**Theo Harness Capability Map**](../../docs/harness-capability-map.md) is the discovery front-door — every harness primitive with its import path, signature, and a one-line example (find `compactTranscript`, `buildRepoMap`, `isTransientError`, `@theokit/sdk/persistence`, ... without reading source). The exported TypeScript types are the canonical contract.
31
+ New here? The [**Theo Harness Capability Map**](https://github.com/usetheodev/theokit-sdk/blob/main/docs/harness-capability-map.md) is the discovery front-door — every harness primitive with its import path, signature, and a one-line example (find `compactTranscript`, `buildRepoMap`, `isTransientError`, `@theokit/sdk/persistence`, ... without reading source). The exported TypeScript types are the canonical contract.
32
+
33
+ It also **ships inside this package** — no network needed, and pinned to the version you installed:
34
+
35
+ ```
36
+ node_modules/@theokit/sdk/docs/harness-capability-map.md # every public primitive + import path
37
+ node_modules/@theokit/sdk/docs/error-codes.md # AgentRunError.code reference
38
+ node_modules/@theokit/sdk/claude-template/ # agent context (npx theokit-init-claude)
39
+ ```
40
+
41
+ Agents that consume documentation should prefer the machine-readable corpora on the docs site ([llmstxt.org](https://llmstxt.org) convention): [`llms.txt`](https://docs.usetheo.dev/llms.txt) (curated index) and [`llms-full.txt`](https://docs.usetheo.dev/llms-full.txt) (every page inlined).
32
42
 
33
43
  ## Install
34
44
 
@@ -50,6 +50,10 @@ console.log(run.aggregate.durationMsP95); // 1830
50
50
  | `Scorers.regex(pattern)` | `pattern.test(output)` — test patterns against adversarial output to avoid ReDoS |
51
51
  | `Scorers.jsonShape(zodSchema, { strict? })` | `JSON.parse(output)` + Zod validation — caps output at 1 MB before parse |
52
52
  | `Scorers.llmJudge({ model, apiKey, criteria, rubric? })` | Second LLM scores against criteria — requires SEPARATE `apiKey` |
53
+ | `Scorers.levenshtein({ threshold?, caseSensitive? })` | Normalized edit-distance similarity `1 - dist/max(len)` — deterministic (no LLM); `threshold` binarizes |
54
+ | `Scorers.numericDiff({ tolerance? })` | Relative numeric closeness `1 - |o-e|/max(|o|,|e|)` — deterministic; `tolerance` binarizes |
55
+ | `Scorers.embeddingSimilarity({ apiKey?, model?, threshold?, embed? })` | Cosine of output vs expected embeddings (OpenRouter by default; inject `embed` for another provider). One embeddings call per row |
56
+ | `Scorers.verifyGate({ sandbox?, repoDir, failToPass, passToPass, command })` | Runs the project's tests in a sandbox; scores 1 iff exit 0 (SWE-bench style) |
53
57
 
54
58
  ### Custom scorer
55
59
 
@@ -105,6 +109,36 @@ interface EvalAggregate {
105
109
 
106
110
  `EvalRun` is plain JSON — `JSON.stringify(run)` works directly.
107
111
 
112
+ ## CI gate — `assertEval`
113
+
114
+ Turn a run into a pass/fail gate. `assertEval` reads only `run.aggregate`,
115
+ collects EVERY unmet threshold, and throws `EvalThresholdError` (with a
116
+ `.failures` list) when any is missed — otherwise returns `void`. Drop it into a
117
+ Vitest `it(...)` or a standalone script whose non-zero exit fails the CI job.
118
+
119
+ ```typescript
120
+ import { Eval, Scorers, assertEval, EvalThresholdError } from "@theokit/sdk/eval";
121
+
122
+ const run = await Eval.create({ name: "qa", dataset, scorers, agent }).run();
123
+
124
+ assertEval(run, {
125
+ minMeanScore: 0.8, // aggregate.meanScore >= 0.8
126
+ minPassRatio: 0.9, // aggregate.passRatio >= 0.9
127
+ maxErrorRatio: 0, // errorRows / totalRows <= 0
128
+ perScorer: { "contains-expected": 0.7 }, // per-scorer mean floor (absent scorer = failure)
129
+ });
130
+ ```
131
+
132
+ ## Trials (smooth non-determinism)
133
+
134
+ `trials: N` runs each dataset row N times and collapses to ONE row whose
135
+ per-scorer score is the mean over the trials (an errored trial contributes 0).
136
+ `EvalRowResult.trialCount` records the collapse. Range `[1, 100]`.
137
+
138
+ ```typescript
139
+ const run = await Eval.create({ name: "qa", dataset, scorers, agent, trials: 3 }).run();
140
+ ```
141
+
108
142
  ## Concurrency
109
143
 
110
144
  `concurrency` defaults to 4. Allowed range: `[1, 64]` (integer). 0 and
@@ -141,4 +175,5 @@ costs roughly $3.00 total (base + judge).
141
175
  | Error | When |
142
176
  |---|---|
143
177
  | `EvalAlreadyRunningError` | Same `name` already running in this process |
178
+ | `EvalThresholdError` | `assertEval` found one or more unmet thresholds (see `.failures`) |
144
179
  | `ConfigurationError` | Invalid concurrency, missing required fields |
package/dist/eval.cjs CHANGED
@@ -7256,11 +7256,11 @@ function truncateToBudget(sources, maxTokens) {
7256
7256
  if (total <= maxTokens) {
7257
7257
  return sources.map((src) => ({ name: src.name, tokens: src.tokens }));
7258
7258
  }
7259
- const floor = Math.min(MIN_SOURCE_TOKENS, Math.floor(maxTokens / sources.length));
7260
- const remaining = Math.max(0, maxTokens - floor * sources.length);
7259
+ const floor2 = Math.min(MIN_SOURCE_TOKENS, Math.floor(maxTokens / sources.length));
7260
+ const remaining = Math.max(0, maxTokens - floor2 * sources.length);
7261
7261
  return sources.map((src) => {
7262
7262
  const proportional = total === 0 ? 0 : Math.floor(src.tokens.length / total * remaining);
7263
- const allotted = Math.min(src.tokens.length, floor + proportional);
7263
+ const allotted = Math.min(src.tokens.length, floor2 + proportional);
7264
7264
  return { name: src.name, tokens: src.tokens.slice(0, allotted) };
7265
7265
  });
7266
7266
  }
@@ -18248,6 +18248,87 @@ function startEvalRunSpan(attrs) {
18248
18248
  };
18249
18249
  }
18250
18250
 
18251
+ // src/internal/eval/trials.ts
18252
+ var TRIAL_INDEX_KEY = "__evalRowIndex";
18253
+ var TRIAL_NUM_KEY = "__evalTrial";
18254
+ function expandForTrials(entries, trials) {
18255
+ const out = [];
18256
+ for (let i = 0; i < entries.length; i += 1) {
18257
+ const entry = entries[i];
18258
+ if (entry === void 0) continue;
18259
+ for (let t = 0; t < trials; t += 1) {
18260
+ out.push({
18261
+ ...entry,
18262
+ metadata: { ...entry.metadata ?? {}, [TRIAL_INDEX_KEY]: i, [TRIAL_NUM_KEY]: t }
18263
+ });
18264
+ }
18265
+ }
18266
+ return out;
18267
+ }
18268
+ function stripReserved(metadata) {
18269
+ if (metadata === void 0) return void 0;
18270
+ const { [TRIAL_INDEX_KEY]: _idx, [TRIAL_NUM_KEY]: _trial, ...rest } = metadata;
18271
+ return Object.keys(rest).length > 0 ? rest : void 0;
18272
+ }
18273
+ function sumDefined(values) {
18274
+ const present = values.filter((v) => typeof v === "number");
18275
+ return present.length > 0 ? present.reduce((acc, v) => acc + v, 0) : void 0;
18276
+ }
18277
+ function meanScoresByScorer(group, trials) {
18278
+ const names = [];
18279
+ const sums = /* @__PURE__ */ new Map();
18280
+ for (const row of group) {
18281
+ for (const s of row.scores) {
18282
+ if (!sums.has(s.name)) {
18283
+ names.push(s.name);
18284
+ sums.set(s.name, 0);
18285
+ }
18286
+ sums.set(s.name, (sums.get(s.name) ?? 0) + s.score);
18287
+ }
18288
+ }
18289
+ return names.map((name) => ({
18290
+ name,
18291
+ score: (sums.get(name) ?? 0) / trials,
18292
+ reason: `mean of ${trials} trials`
18293
+ }));
18294
+ }
18295
+ function collapseGroup(index, group, trials) {
18296
+ const first = group[0];
18297
+ const scores = meanScoresByScorer(group, trials);
18298
+ const allErrored = group.every((r) => r.error !== void 0);
18299
+ const meanScore = allErrored || scores.length === 0 ? 0 : scores.reduce((acc, s) => acc + s.score, 0) / scores.length;
18300
+ const firstOk = group.find((r) => r.error === void 0);
18301
+ const errorMsg = allErrored ? group.find((r) => r.error !== void 0)?.error : void 0;
18302
+ const metadata = stripReserved(first.metadata);
18303
+ const tokensIn = sumDefined(group.map((r) => r.tokensIn));
18304
+ const tokensOut = sumDefined(group.map((r) => r.tokensOut));
18305
+ return {
18306
+ index,
18307
+ input: first.input,
18308
+ output: firstOk?.output ?? first.output,
18309
+ ...first.expected !== void 0 ? { expected: first.expected } : {},
18310
+ scores,
18311
+ meanScore,
18312
+ durationMs: group.reduce((acc, r) => acc + r.durationMs, 0),
18313
+ ...tokensIn !== void 0 ? { tokensIn } : {},
18314
+ ...tokensOut !== void 0 ? { tokensOut } : {},
18315
+ ...errorMsg !== void 0 ? { error: errorMsg } : {},
18316
+ ...metadata !== void 0 ? { metadata } : {},
18317
+ trialCount: trials
18318
+ };
18319
+ }
18320
+ function collapseTrials(rows, trials) {
18321
+ const groups = /* @__PURE__ */ new Map();
18322
+ for (const row of rows) {
18323
+ const meta = row.metadata;
18324
+ const origIdx = typeof meta?.[TRIAL_INDEX_KEY] === "number" ? meta[TRIAL_INDEX_KEY] : row.index;
18325
+ const arr = groups.get(origIdx) ?? [];
18326
+ arr.push(row);
18327
+ groups.set(origIdx, arr);
18328
+ }
18329
+ return [...groups.entries()].sort((a, b) => a[0] - b[0]).map(([origIdx, group]) => collapseGroup(origIdx, group, trials));
18330
+ }
18331
+
18251
18332
  // src/internal/eval/runner.ts
18252
18333
  function safeHook(fn) {
18253
18334
  try {
@@ -18456,7 +18537,9 @@ async function runEval(options, runOpts) {
18456
18537
  const id = crypto.randomUUID();
18457
18538
  const startedAt = Date.now();
18458
18539
  const entries = await materializeDataset(options.dataset);
18459
- const indexed = entries.map((e) => ({ ...e }));
18540
+ const materialized = entries.map((e) => ({ ...e }));
18541
+ const trials = options.trials ?? 1;
18542
+ const indexed = trials > 1 ? expandForTrials(materialized, trials) : materialized;
18460
18543
  const scorers = normalizeScorers(options.scorers);
18461
18544
  const concurrency = options.concurrency ?? 4;
18462
18545
  const signal = runOpts?.signal;
@@ -18488,7 +18571,8 @@ async function runEval(options, runOpts) {
18488
18571
  const batchOpts = makeAgentForBatch(options.agent, indexed);
18489
18572
  rows = await runRowsViaBatch(indexed, batchOpts, scorers, concurrency, signal, onRow, sink);
18490
18573
  }
18491
- const aggregate = computeAggregate(rows);
18574
+ const finalRows = trials > 1 ? collapseTrials(rows, trials) : rows;
18575
+ const aggregate = computeAggregate(finalRows);
18492
18576
  const endedAt = Date.now();
18493
18577
  const run = {
18494
18578
  id,
@@ -18497,7 +18581,7 @@ async function runEval(options, runOpts) {
18497
18581
  endedAt,
18498
18582
  durationMs: endedAt - startedAt,
18499
18583
  aggregate,
18500
- rows,
18584
+ rows: finalRows,
18501
18585
  ...options.metadata !== void 0 ? { metadata: options.metadata } : {}
18502
18586
  };
18503
18587
  safeHook(() => hooks?.afterRun?.(run));
@@ -18512,6 +18596,59 @@ async function runEval(options, runOpts) {
18512
18596
  }
18513
18597
  }
18514
18598
 
18599
+ // src/internal/eval/assert.ts
18600
+ var EvalThresholdError = class extends Error {
18601
+ name = "EvalThresholdError";
18602
+ /** The eval's name (`EvalRun.name`). */
18603
+ evalName;
18604
+ /** Every unmet threshold, in check order. */
18605
+ failures;
18606
+ constructor(evalName, failures) {
18607
+ const lines = failures.map(
18608
+ (f) => ` - ${f.metric}: required ${f.threshold}, got ${Number.isNaN(f.actual) ? "n/a (scorer absent)" : f.actual}`
18609
+ );
18610
+ super(
18611
+ `Eval "${evalName}" failed ${failures.length} threshold${failures.length === 1 ? "" : "s"}:
18612
+ ${lines.join("\n")}`
18613
+ );
18614
+ this.evalName = evalName;
18615
+ this.failures = failures;
18616
+ }
18617
+ };
18618
+ function floor(metric, actual, threshold) {
18619
+ if (threshold === void 0 || actual >= threshold) return void 0;
18620
+ return { metric, threshold, actual };
18621
+ }
18622
+ function perScorerFailures(perScorer, thresholds) {
18623
+ if (thresholds === void 0) return [];
18624
+ const out = [];
18625
+ for (const [name, min] of Object.entries(thresholds)) {
18626
+ const stats = perScorer[name];
18627
+ if (stats === void 0 || stats.mean < min) {
18628
+ out.push({
18629
+ metric: `perScorer.${name}`,
18630
+ threshold: min,
18631
+ actual: stats === void 0 ? Number.NaN : stats.mean
18632
+ });
18633
+ }
18634
+ }
18635
+ return out;
18636
+ }
18637
+ function assertEval(run, thresholds) {
18638
+ const a = run.aggregate;
18639
+ const errorRatio = a.totalRows > 0 ? a.errorRows / a.totalRows : 0;
18640
+ const ceiling = thresholds.maxErrorRatio !== void 0 && errorRatio > thresholds.maxErrorRatio ? { metric: "errorRatio", threshold: thresholds.maxErrorRatio, actual: errorRatio } : void 0;
18641
+ const failures = [
18642
+ floor("meanScore", a.meanScore, thresholds.minMeanScore),
18643
+ floor("passRatio", a.passRatio, thresholds.minPassRatio),
18644
+ ceiling,
18645
+ ...perScorerFailures(a.perScorer, thresholds.perScorer)
18646
+ ].filter((f) => f !== void 0);
18647
+ if (failures.length > 0) {
18648
+ throw new EvalThresholdError(run.name, failures);
18649
+ }
18650
+ }
18651
+
18515
18652
  // src/sandbox/shell-escape.ts
18516
18653
  function shellEscapePosix(arg) {
18517
18654
  return `'${arg.replace(/'/g, "'\\''")}'`;
@@ -18531,6 +18668,27 @@ async function captureArtifact(sandbox, repoDir) {
18531
18668
  return { diff, applies: check.exitCode === 0 };
18532
18669
  }
18533
18670
 
18671
+ // src/internal/scorers/levenshtein.ts
18672
+ var LEVENSHTEIN_MAX_LEN = 4e3;
18673
+ function nextRow(prev, rowIndex, ai, b) {
18674
+ const curr = [rowIndex];
18675
+ for (let j = 1; j <= b.length; j += 1) {
18676
+ const cost = ai === b.charCodeAt(j - 1) ? 0 : 1;
18677
+ curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
18678
+ }
18679
+ return curr;
18680
+ }
18681
+ function levenshteinDistance(a, b) {
18682
+ const m = a.length;
18683
+ const n = b.length;
18684
+ if (m === 0) return n;
18685
+ if (n === 0) return m;
18686
+ let prev = [];
18687
+ for (let j = 0; j <= n; j += 1) prev[j] = j;
18688
+ for (let i = 1; i <= m; i += 1) prev = nextRow(prev, i, a.charCodeAt(i - 1), b);
18689
+ return prev[n] ?? 0;
18690
+ }
18691
+
18534
18692
  // src/internal/scorers/llm-judge.ts
18535
18693
  init_agent_factory_registry();
18536
18694
  function buildPrompt(subject, criteria, rubric, expected) {
@@ -18691,15 +18849,80 @@ var LocalSandbox = class extends SandboxBackend {
18691
18849
 
18692
18850
  // src/scorers.ts
18693
18851
  var JSON_SHAPE_MAX_BYTES = 1e6;
18852
+ function cosineSimilarity(a, b) {
18853
+ const n = Math.min(a.length, b.length);
18854
+ let dot = 0;
18855
+ let na = 0;
18856
+ let nb = 0;
18857
+ for (let i = 0; i < n; i += 1) {
18858
+ const av = a[i] ?? 0;
18859
+ const bv = b[i] ?? 0;
18860
+ dot += av * bv;
18861
+ na += av * av;
18862
+ nb += bv * bv;
18863
+ }
18864
+ if (na === 0 || nb === 0) return 0;
18865
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
18866
+ }
18867
+ function normalizeStringInputs(output, expected, caseSensitive) {
18868
+ if (typeof expected !== "string") return { score: 0, reason: "expected_not_string" };
18869
+ if (expected.length === 0) return { score: 0, reason: "expected_empty" };
18870
+ return {
18871
+ o: caseSensitive ? output : output.toLowerCase(),
18872
+ e: caseSensitive ? expected : expected.toLowerCase()
18873
+ };
18874
+ }
18875
+ function scoreLevenshtein(output, expected, caseSensitive, threshold) {
18876
+ const norm = normalizeStringInputs(output, expected, caseSensitive);
18877
+ if (!("o" in norm)) return norm;
18878
+ const { o, e } = norm;
18879
+ if (o.length > LEVENSHTEIN_MAX_LEN || e.length > LEVENSHTEIN_MAX_LEN) {
18880
+ return { score: 0, reason: "input_too_large" };
18881
+ }
18882
+ const sim = 1 - levenshteinDistance(o, e) / Math.max(o.length, e.length, 1);
18883
+ if (threshold === void 0) return { score: sim };
18884
+ return sim >= threshold ? { score: 1 } : { score: 0, reason: `sim=${sim.toFixed(3)}` };
18885
+ }
18886
+ function toFiniteNumber2(value) {
18887
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
18888
+ if (value === void 0 || value === null) return void 0;
18889
+ const n = Number(String(value).trim());
18890
+ return Number.isFinite(n) ? n : void 0;
18891
+ }
18892
+ function scoreNumericDiff(output, expected, tolerance) {
18893
+ const o = toFiniteNumber2(output);
18894
+ if (o === void 0) return { score: 0, reason: "output_not_numeric" };
18895
+ const e = toFiniteNumber2(expected);
18896
+ if (e === void 0) return { score: 0, reason: "expected_not_numeric" };
18897
+ if (tolerance !== void 0) {
18898
+ return Math.abs(o - e) <= tolerance ? { score: 1 } : { score: 0, reason: `abs_diff=${Math.abs(o - e)}` };
18899
+ }
18900
+ const denom = Math.max(Math.abs(o), Math.abs(e));
18901
+ if (denom === 0) return { score: 1 };
18902
+ return { score: Math.max(0, 1 - Math.abs(o - e) / denom) };
18903
+ }
18904
+ function embedderCreateOptions(opts) {
18905
+ return {
18906
+ ...opts.model !== void 0 ? { model: opts.model } : {},
18907
+ ...opts.apiKey !== void 0 ? { apiKey: opts.apiKey } : {},
18908
+ ...opts.baseUrl !== void 0 ? { baseUrl: opts.baseUrl } : {}
18909
+ };
18910
+ }
18911
+ function makeDefaultEmbedder(opts) {
18912
+ let runtime;
18913
+ return (texts) => {
18914
+ if (runtime === void 0) {
18915
+ runtime = openRouterMemoryEmbeddingProviderAdapter.create(embedderCreateOptions(opts));
18916
+ }
18917
+ return runtime.then((rt) => rt.embed(texts));
18918
+ };
18919
+ }
18694
18920
  function makeStringScorer(name, caseSensitive, compare) {
18695
18921
  return {
18696
18922
  name,
18697
18923
  score: (output, expected) => {
18698
- if (typeof expected !== "string") return { score: 0, reason: "expected_not_string" };
18699
- if (expected.length === 0) return { score: 0, reason: "expected_empty" };
18700
- const o = caseSensitive ? output : output.toLowerCase();
18701
- const e = caseSensitive ? expected : expected.toLowerCase();
18702
- return compare(o, e);
18924
+ const norm = normalizeStringInputs(output, expected, caseSensitive);
18925
+ return "o" in norm ? compare(norm.o, norm.e) : norm;
18703
18926
  }
18704
18927
  };
18705
18928
  }
@@ -18743,6 +18966,56 @@ var Scorers = {
18743
18966
  }
18744
18967
  };
18745
18968
  },
18969
+ /**
18970
+ * SE41 — normalized Levenshtein similarity: `1 - editDistance / max(len)`.
18971
+ * Deterministic (no LLM), so it always runs in CI. `threshold` binarizes.
18972
+ *
18973
+ * Refuses empty/non-string `expected` (EC-1 parity) and caps input at
18974
+ * {@link LEVENSHTEIN_MAX_LEN} chars to bound the O(n*m) cost on adversarial output.
18975
+ */
18976
+ levenshtein(opts = {}) {
18977
+ const caseSensitive = opts.caseSensitive ?? false;
18978
+ const { threshold } = opts;
18979
+ return {
18980
+ name: threshold !== void 0 ? `levenshtein(>=${threshold})` : "levenshtein",
18981
+ score: (output, expected) => scoreLevenshtein(output, expected, caseSensitive, threshold)
18982
+ };
18983
+ },
18984
+ /**
18985
+ * SE41 — numeric closeness. Parses `output` and `expected` as numbers and
18986
+ * scores continuous relative closeness `1 - |o-e| / max(|o|,|e|)` (both 0 ⇒ 1),
18987
+ * or a binary pass when `tolerance` is set. Deterministic (no LLM).
18988
+ */
18989
+ numericDiff(opts = {}) {
18990
+ const { tolerance } = opts;
18991
+ return {
18992
+ name: "numeric-diff",
18993
+ score: (output, expected) => scoreNumericDiff(output, expected, tolerance)
18994
+ };
18995
+ },
18996
+ /**
18997
+ * SE41 — semantic similarity via embeddings: cosine of `embed(output)` vs
18998
+ * `embed(expected)`, clamped to `[0, 1]` (negatives → 0). `threshold` binarizes.
18999
+ *
19000
+ * By default routes through OpenRouter's embeddings endpoint
19001
+ * (`OPENROUTER_API_KEY`); inject `embed` to use another provider or to test
19002
+ * deterministically. Each scored row costs one embeddings call.
19003
+ */
19004
+ embeddingSimilarity(opts) {
19005
+ const { threshold } = opts;
19006
+ const embed = opts.embed ?? makeDefaultEmbedder(opts);
19007
+ return {
19008
+ name: threshold !== void 0 ? `embedding-similarity(>=${threshold})` : "embedding-similarity",
19009
+ score: async (output, expected) => {
19010
+ if (typeof expected !== "string") return { score: 0, reason: "expected_not_string" };
19011
+ const [vo, ve] = await embed([output, expected]);
19012
+ if (vo === void 0 || ve === void 0) return { score: 0, reason: "embed_failed" };
19013
+ const score = Math.max(0, cosineSimilarity(vo, ve));
19014
+ if (threshold === void 0) return { score };
19015
+ return score >= threshold ? { score: 1 } : { score: 0, reason: `cos=${score.toFixed(3)}` };
19016
+ }
19017
+ };
19018
+ },
18746
19019
  /**
18747
19020
  * Parse `output` as JSON and validate against a Zod schema.
18748
19021
  *
@@ -18848,6 +19121,9 @@ var EvalOptionsSchema = zod.z.object({
18848
19121
  // EC-3: concurrency MUST be a finite integer in [1, 64]. Bare `z.number()`
18849
19122
  // would let Infinity / 0 / negatives slip through.
18850
19123
  concurrency: zod.z.number().int("concurrency must be an integer").min(1, "concurrency must be >= 1").max(64, "concurrency must be <= 64").optional(),
19124
+ // SE41: trials MUST be a finite integer in [1, 100]. Guards against 0
19125
+ // (no-op run) and unbounded fanout that would DoS the provider.
19126
+ trials: zod.z.number().int("trials must be an integer").min(1, "trials must be >= 1").max(100, "trials must be <= 100").optional(),
18851
19127
  metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
18852
19128
  hooks: zod.z.object({
18853
19129
  beforeRun: zod.z.function().optional(),
@@ -18880,8 +19156,10 @@ var Eval = class _Eval {
18880
19156
 
18881
19157
  exports.Eval = Eval;
18882
19158
  exports.EvalAlreadyRunningError = EvalAlreadyRunningError;
19159
+ exports.EvalThresholdError = EvalThresholdError;
18883
19160
  exports.JsonlParseError = JsonlParseError;
18884
19161
  exports.Scorers = Scorers;
19162
+ exports.assertEval = assertEval;
18885
19163
  exports.captureArtifact = captureArtifact;
18886
19164
  exports.loadJsonl = loadJsonl;
18887
19165
  //# sourceMappingURL=eval.cjs.map