@gaunt-sloth/batch 2.0.0-alpha.19 → 2.0.0-alpha.20

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/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @gaunt-sloth/batch
2
+
3
+ The batch / eval / workflow runtime for Gaunt Sloth. You reach it three ways: through the commands
4
+ the `gaunt-sloth` app registers (`gth batch`, `gth eval`, `gth workflow`), through this package's own
5
+ `gth-batch` binary — a thin standalone matrix runner for shell pipelines (see [below](#pipeline-runner-gth-batch)) —
6
+ or by importing the package to embed the runtime. Install `gaunt-sloth` for the full command set;
7
+ install `@gaunt-sloth/batch` for the standalone binary, or depend on it to embed the runtime.
8
+
9
+ ## Grade a suite of prompts (`gth eval`)
10
+
11
+ You want to check that your agent still passes a set of graded cases before you ship a prompt or
12
+ config change. Write a suite YAML, install the CLI, then run `gth eval` — it exits `0` iff every
13
+ case passes ("pytest for prompts"). The commands run on `@gaunt-sloth/batch`, but they ship in the
14
+ `gaunt-sloth` app, so that is what you install:
15
+
16
+ ```bash
17
+ npm install -g gaunt-sloth@alpha
18
+ ```
19
+
20
+ Write `prompts.eval.yaml`:
21
+
22
+ ```yaml
23
+ target: { type: gth-agent }
24
+ cases:
25
+ - id: greets-and-signs-off
26
+ prompt: "Greet the user, then say goodbye."
27
+ must_contain: ["hello", "goodbye"]
28
+ - id: summarizes-as-json
29
+ prompt: "Summarize the last release as a JSON object with a title field."
30
+ must_not_contain: ["Sorry"]
31
+ judge: "Returns a single JSON object with a non-empty title field."
32
+ pass_threshold: 7
33
+ ```
34
+
35
+ Run it:
36
+
37
+ ```bash
38
+ gth eval prompts.eval.yaml
39
+ ```
40
+
41
+ A case passes when its deterministic checks hold (`must_contain` / `must_not_contain` /
42
+ `should_contain_any`) and, if a `judge` rubric is set, the LLM judge rates the answer at or above
43
+ `pass_threshold` (0–10 scale, suite default `6`). `gth eval` prints a `PASS`/`FAIL` line per case
44
+ plus a suite total, writes structured per-case JSON and a `results.json` summary to a timestamped
45
+ output dir (override with `-o <dir>`), and exits non-zero if any case failed. Add `-j <n>` to cap
46
+ in-flight cases.
47
+
48
+ ### Examples
49
+
50
+ ```bash
51
+ # Run one prompt-executable over a matrix of two models × the rows of a CSV.
52
+ gth batch summarize.md --over inputs.csv --models gemini-2.5-pro,gemini-3.5-flash -j 4
53
+
54
+ # Run a local orchestration script, passing it JSON as ctx.args.
55
+ gth workflow rank-models.mjs --args '{"topic":"robotics"}'
56
+ ```
57
+
58
+ `gth batch <script.md>` runs a markdown prompt-executable over a matrix of models (`--models a,b,c`,
59
+ comma-separated; omit to use the configured model) and/or content-bound input rows (`--over
60
+ <file.csv|file.jsonl>` — one cell per row, with `{{field}}` placeholders bound from the row). It
61
+ writes the same structured per-cell output as `eval` but — unlike `eval` — exits `0` as long as the
62
+ cells *ran*: a poor answer is not a harness failure. `-j <n>` caps concurrency, `--retry <n>` retries
63
+ a failed cell (default `0`), `-o <dir>` sets the output dir.
64
+
65
+ `gth workflow <script.mjs>` runs a local ESM script whose default export is `async (ctx) => result`;
66
+ the return value is printed (a string as-is, anything else as pretty JSON), and `--args <json>` is
67
+ handed to the script as `ctx.args`. The script is arbitrary local ESM run with full Node privileges
68
+ (it can read files and spawn processes) — run only scripts you trust, as you would any local script.
69
+
70
+ ## Pipeline runner (`gth-batch`)
71
+
72
+ `gth batch` above is the full command, wired into the `gaunt-sloth` app. When you want the matrix
73
+ runtime on its own — inside a shell pipeline, without installing the whole CLI — this package ships a
74
+ thin binary, `gth-batch`, that runs the same matrix and emits the **same per-cell records** `gth batch`
75
+ produces, one JSON object per line (JSONL) on stdout:
76
+
77
+ ```bash
78
+ # Fan a prompt-executable over rows piped in as JSON (or YAML), across two models.
79
+ echo '[{"topic":"gears"},{"topic":"levers"}]' \
80
+ | gth-batch explain.md --models gemini-2.5-flash,gemini-2.5-pro -j 4 \
81
+ | jq -c 'select(.ok) | {id, model, answer}'
82
+ ```
83
+
84
+ It takes the script path as its argument and the input axis as **inline `--over` data** (a JSON/YAML
85
+ array of row objects) **or on stdin** — a pipeline already has the shell to produce the data, so
86
+ unlike `gth batch` there is no CSV/JSONL file path. `--models a,b,c`, `-j <n>` (concurrency) and
87
+ `--retry <n>` behave as in `gth batch`.
88
+
89
+ Each stdout line is the full `CellResult` (`id`, `model`, `inputIndex`, `inputRow`, `ok`, `answer`,
90
+ `tokensInput`/`tokensOutput`, `tools`, `durationMs`, `retries`) — stdout is kept a clean data channel
91
+ (all progress/errors go to stderr), so it pipes straight into `jq`, `grep`, or a file. Following the
92
+ `gth batch` exit-code contract, `gth-batch` exits `0` as long as the cells *ran* — a poor or failed
93
+ cell is recorded as `"ok": false` in its line, not reflected in the exit code; only a harness error
94
+ (bad arguments, an unreadable script, malformed `--over`, or a config failure) exits non-zero.
95
+
96
+ It resolves your model from the same `.gsloth.config.*` as the rest of Gaunt Sloth (discovered from
97
+ the working directory), so configure a provider there first.
98
+
99
+ ## Programmatic use
100
+
101
+ Depend on `@gaunt-sloth/batch` to drive the same engine from your own code. Every module is exported
102
+ via the `./*.js` subpath map, matching the other `@gaunt-sloth/*` packages:
103
+
104
+ ```js
105
+ import { runEvalSuite } from '@gaunt-sloth/batch/evalRunner.js';
106
+ ```
107
+
108
+ The public API (see the package's `index.ts`) groups by command:
109
+
110
+ - **Matrix** (`gth batch`): `buildMatrix`, `bindCellContent`, `parseOverFile`, `runBatchMatrix`,
111
+ `buildBatchSummary`, `writeBatchOutput`, `DEFAULT_CONCURRENCY`.
112
+ - **Eval** (`gth eval`): `parseEvalSuite`, `runDeterministicChecks`, `judgeEvalCase`, `runEvalSuite`,
113
+ `writeEvalOutput`, `EvalVerdictSchema`, `DEFAULT_EVAL_PASS_THRESHOLD`.
114
+ - **Workflow** (`gth workflow`): `runWorkflow`.
115
+
116
+ The corresponding TypeScript types (`MatrixCell`, `BatchSummary`, `CellResult`, `EvalSuite`,
117
+ `EvalCaseResult`, `WorkflowContext`, …) are exported alongside them.
118
+
119
+ ## Dependencies
120
+
121
+ - `@gaunt-sloth/core` — config, provider factory, the lean single-shot runtime, `askStructured`
122
+ - `@gaunt-sloth/agent` — the resolvers and lean agent factory the batch/eval/workflow cell path runs on
123
+ - `@langchain/core`, `yaml`, `zod`
124
+
125
+ ## Related packages
126
+
127
+ - [`@gaunt-sloth/core`](https://www.npmjs.com/package/@gaunt-sloth/core) — Core utilities, config,
128
+ and agent infrastructure
129
+ ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/core))
130
+ - [`@gaunt-sloth/agent`](https://www.npmjs.com/package/@gaunt-sloth/agent) — Agent runtime: built-in
131
+ tools, filesystem toolkit, middleware registry, API server, AG-UI, MCP, and A2A integration
132
+ ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/agent))
133
+ - [`@gaunt-sloth/review`](https://www.npmjs.com/package/@gaunt-sloth/review) — Review and Q&A modules
134
+ with standalone CLI
135
+ ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/review))
136
+ - [`@gaunt-sloth/batch`](https://www.npmjs.com/package/@gaunt-sloth/batch) — Batch / eval / workflow
137
+ runtime (this package)
138
+ ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/batch))
139
+ - [`gaunt-sloth`](https://www.npmjs.com/package/gaunt-sloth) — Main CLI application
140
+ ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/app))
package/dist/bin.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @module bin
4
+ *
5
+ * BATCH-9 — the `gth-batch` executable. A minimal shebang wrapper around {@link runBatchCli}
6
+ * (pipelineCli.ts) whose only job beyond delegating is to keep **stdout a clean machine channel**: the
7
+ * batch runtime's human/status/streaming output all lands on `process.stdout` (via
8
+ * `console.*`/`ProgressIndicator`/`stream()`), so this redirects `process.stdout.write` to stderr
9
+ * for the duration of the run. The JSONL cell records are written straight to fd 1 inside
10
+ * `runBatchCli` (`fs.writeSync`), bypassing this redirect — the same "protocol channel" discipline
11
+ * `packages/app/cli.js` uses for the ACP stdio channel.
12
+ */
13
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @module bin
4
+ *
5
+ * BATCH-9 — the `gth-batch` executable. A minimal shebang wrapper around {@link runBatchCli}
6
+ * (pipelineCli.ts) whose only job beyond delegating is to keep **stdout a clean machine channel**: the
7
+ * batch runtime's human/status/streaming output all lands on `process.stdout` (via
8
+ * `console.*`/`ProgressIndicator`/`stream()`), so this redirects `process.stdout.write` to stderr
9
+ * for the duration of the run. The JSONL cell records are written straight to fd 1 inside
10
+ * `runBatchCli` (`fs.writeSync`), bypassing this redirect — the same "protocol channel" discipline
11
+ * `packages/app/cli.js` uses for the ACP stdio channel.
12
+ */
13
+ import { runBatchCli } from '#src/pipelineCli.js';
14
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ process.stdout.write = ((chunk, encoding, cb) => process.stderr.write(chunk, encoding, cb));
17
+ runBatchCli(process.argv.slice(2))
18
+ .then((code) => {
19
+ process.exitCode = code;
20
+ })
21
+ .catch((err) => {
22
+ process.stderr.write(`gth-batch: ${err instanceof Error ? err.message : String(err)}\n`);
23
+ process.exitCode = 1;
24
+ })
25
+ .finally(() => {
26
+ process.stdout.write = originalStdoutWrite;
27
+ });
28
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,MAAM,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AACtE,8DAA8D;AAC9D,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,KAAU,EAAE,QAAc,EAAE,EAAQ,EAAW,EAAE,CACxE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAgC,CAAC;AAE5E,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;KAC/B,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;IACb,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,cAAc,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC;KACD,OAAO,CAAC,GAAG,EAAE;IACZ,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,mBAAmB,CAAC;AAC7C,CAAC,CAAC,CAAC"}
@@ -1,16 +1,48 @@
1
- import type { DeterministicCheckResult, EvalCase } from '#src/evalTypes.js';
1
+ import type { DeterministicCheckResult, EvalExpectation } from '#src/evalTypes.js';
2
2
  /**
3
- * Case-insensitive substring checks over an SUT answer. Ported, not reinvented, from the field
4
- * user's proven `deterministic()` function (`docs/batch-eval-user-requirements.md` Appendix A):
3
+ * Resolve a **minimal** JSON path (BATCH-10) against an already-parsed JSON value.
4
+ *
5
+ * Supported subset (deliberately tiny, dependency-free — NOT full JSONPath):
6
+ * - an optional leading `$` and/or leading `.` (`$.items[0]`, `.items[0]`, and `items[0]` are
7
+ * equivalent);
8
+ * - dot-separated object keys (`data.rows`);
9
+ * - `[<int>]` array indexing (`items[0]`, `rows[2].id`).
10
+ *
11
+ * No wildcards, filters, slices, quoted keys, or negative indices. A segment made only of digits
12
+ * is treated as an array index; any other segment is an object key. Returns `{ found: false }` for
13
+ * any missing key, out-of-range index, or type mismatch (indexing a non-array, keying a non-object)
14
+ * rather than throwing.
15
+ */
16
+ export declare function resolveJsonPath(root: unknown, path: string): {
17
+ found: boolean;
18
+ value?: unknown;
19
+ };
20
+ /**
21
+ * Deterministic, answer-based checks over an SUT answer. The substring family is ported, not
22
+ * reinvented, from the field user's proven `deterministic()` function
23
+ * (`docs/batch-eval-user-requirements.md` Appendix A); BATCH-10 adds regex and minimal JSON-path
24
+ * assertions alongside them (all over the *answer* — tool-trace assertions live in
25
+ * `#src/toolChecks.js`, since they read the tool names, not the answer):
5
26
  *
6
27
  * - `mustContain` — every entry must appear (case-insensitive substring); each miss is reported.
7
28
  * - `mustNotContain` — no entry may appear; each hit is reported.
8
29
  * - `shouldContainAny` — at least one entry must appear; reported as a single combined failure
9
30
  * (not one per missing option — the check is "at least one", so there is only one way to fail
10
31
  * it) when none do.
32
+ * - `mustMatch` — every regex must match the raw answer (no case-folding: the pattern owns its
33
+ * flags); each miss is reported as `answer did not match /…/`.
34
+ * - `mustNotMatch` — no regex may match; each hit is reported as `answer matched forbidden /…/`.
35
+ * - `jsonPath` — the answer is parsed as JSON and each path assertion (`equals`/`contains`) is
36
+ * checked; a non-JSON answer fails the whole group once.
37
+ *
38
+ * Failures are ordered substring → regex → json_path, so a substring-only case produces exactly the
39
+ * same output it did before BATCH-10. An expectation with every array empty trivially passes (no
40
+ * checks to fail) — the suite parser (`#src/evalSuite.js`) is what enforces that a block has *some*
41
+ * check or a judge rubric; this function itself has no opinion on that.
11
42
  *
12
- * A case with all three arrays empty trivially passes (no checks to fail) — the suite parser
13
- * (`#src/evalSuite.js`) is what enforces that a case has *some* check or a judge rubric; this
14
- * function itself has no opinion on that.
43
+ * BATCH-12: grades one {@link EvalExpectation} block (the assertion bundle) — a flat case's single
44
+ * unscoped block or one of a matrix case's identity-scoped `expect:` blocks. The signature is the
45
+ * same `Pick`/`Partial` shape as before, just re-based on `EvalExpectation` (identical field names),
46
+ * so the answer-check behavior is unchanged.
15
47
  */
16
- export declare function runDeterministicChecks(answer: string, evalCase: Pick<EvalCase, 'mustContain' | 'mustNotContain' | 'shouldContainAny'>): DeterministicCheckResult;
48
+ export declare function runDeterministicChecks(answer: string, expectation: Pick<EvalExpectation, 'mustContain' | 'mustNotContain' | 'shouldContainAny'> & Partial<Pick<EvalExpectation, 'mustMatch' | 'mustNotMatch' | 'jsonPath'>>): DeterministicCheckResult;
@@ -1,34 +1,143 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ /** Render a compiled pattern back as `/source/flags` for failure messages. */
3
+ function formatRegex(re) {
4
+ return `/${re.source}/${re.flags}`;
5
+ }
1
6
  /**
2
- * Case-insensitive substring checks over an SUT answer. Ported, not reinvented, from the field
3
- * user's proven `deterministic()` function (`docs/batch-eval-user-requirements.md` Appendix A):
7
+ * Resolve a **minimal** JSON path (BATCH-10) against an already-parsed JSON value.
8
+ *
9
+ * Supported subset (deliberately tiny, dependency-free — NOT full JSONPath):
10
+ * - an optional leading `$` and/or leading `.` (`$.items[0]`, `.items[0]`, and `items[0]` are
11
+ * equivalent);
12
+ * - dot-separated object keys (`data.rows`);
13
+ * - `[<int>]` array indexing (`items[0]`, `rows[2].id`).
14
+ *
15
+ * No wildcards, filters, slices, quoted keys, or negative indices. A segment made only of digits
16
+ * is treated as an array index; any other segment is an object key. Returns `{ found: false }` for
17
+ * any missing key, out-of-range index, or type mismatch (indexing a non-array, keying a non-object)
18
+ * rather than throwing.
19
+ */
20
+ export function resolveJsonPath(root, path) {
21
+ let p = path.trim();
22
+ if (p.startsWith('$'))
23
+ p = p.slice(1);
24
+ if (p.startsWith('.'))
25
+ p = p.slice(1);
26
+ // Normalize `[n]` index syntax into dot segments so a single split handles both forms.
27
+ p = p.replace(/\[(\d+)\]/g, '.$1');
28
+ const segments = p.split('.').filter((segment) => segment.length > 0);
29
+ let current = root;
30
+ for (const segment of segments) {
31
+ if (current === null || typeof current !== 'object') {
32
+ return { found: false };
33
+ }
34
+ if (/^\d+$/.test(segment)) {
35
+ const index = Number(segment);
36
+ if (!Array.isArray(current) || index >= current.length) {
37
+ return { found: false };
38
+ }
39
+ current = current[index];
40
+ }
41
+ else {
42
+ if (Array.isArray(current) || !Object.prototype.hasOwnProperty.call(current, segment)) {
43
+ return { found: false };
44
+ }
45
+ current = current[segment];
46
+ }
47
+ }
48
+ return { found: true, value: current };
49
+ }
50
+ /** Run the `json_path` assertions over the answer parsed as JSON. If the answer is not valid JSON,
51
+ * the whole group fails with a single reason (not one per entry) and no entry is evaluated. */
52
+ function runJsonPathChecks(answer, checks) {
53
+ if (checks.length === 0)
54
+ return [];
55
+ let root;
56
+ try {
57
+ root = JSON.parse(answer.trim());
58
+ }
59
+ catch {
60
+ return ['answer is not JSON (json_path checks require a JSON answer)'];
61
+ }
62
+ const failures = [];
63
+ for (const check of checks) {
64
+ const { found, value } = resolveJsonPath(root, check.path);
65
+ if (!found) {
66
+ failures.push(`json_path "${check.path}" did not resolve (no such path in answer)`);
67
+ continue;
68
+ }
69
+ if (check.contains !== undefined) {
70
+ if (typeof value !== 'string') {
71
+ failures.push(`json_path "${check.path}" is ${JSON.stringify(value)} (contains check requires a string)`);
72
+ }
73
+ else if (!value.includes(check.contains)) {
74
+ failures.push(`json_path "${check.path}" does not contain "${check.contains}"`);
75
+ }
76
+ }
77
+ else if (!isDeepStrictEqual(value, check.equals)) {
78
+ failures.push(`json_path "${check.path}" is ${JSON.stringify(value)}, expected ${JSON.stringify(check.equals)}`);
79
+ }
80
+ }
81
+ return failures;
82
+ }
83
+ /**
84
+ * Deterministic, answer-based checks over an SUT answer. The substring family is ported, not
85
+ * reinvented, from the field user's proven `deterministic()` function
86
+ * (`docs/batch-eval-user-requirements.md` Appendix A); BATCH-10 adds regex and minimal JSON-path
87
+ * assertions alongside them (all over the *answer* — tool-trace assertions live in
88
+ * `#src/toolChecks.js`, since they read the tool names, not the answer):
4
89
  *
5
90
  * - `mustContain` — every entry must appear (case-insensitive substring); each miss is reported.
6
91
  * - `mustNotContain` — no entry may appear; each hit is reported.
7
92
  * - `shouldContainAny` — at least one entry must appear; reported as a single combined failure
8
93
  * (not one per missing option — the check is "at least one", so there is only one way to fail
9
94
  * it) when none do.
95
+ * - `mustMatch` — every regex must match the raw answer (no case-folding: the pattern owns its
96
+ * flags); each miss is reported as `answer did not match /…/`.
97
+ * - `mustNotMatch` — no regex may match; each hit is reported as `answer matched forbidden /…/`.
98
+ * - `jsonPath` — the answer is parsed as JSON and each path assertion (`equals`/`contains`) is
99
+ * checked; a non-JSON answer fails the whole group once.
10
100
  *
11
- * A case with all three arrays empty trivially passes (no checks to fail) the suite parser
12
- * (`#src/evalSuite.js`) is what enforces that a case has *some* check or a judge rubric; this
13
- * function itself has no opinion on that.
101
+ * Failures are ordered substring regex json_path, so a substring-only case produces exactly the
102
+ * same output it did before BATCH-10. An expectation with every array empty trivially passes (no
103
+ * checks to fail) the suite parser (`#src/evalSuite.js`) is what enforces that a block has *some*
104
+ * check or a judge rubric; this function itself has no opinion on that.
105
+ *
106
+ * BATCH-12: grades one {@link EvalExpectation} block (the assertion bundle) — a flat case's single
107
+ * unscoped block or one of a matrix case's identity-scoped `expect:` blocks. The signature is the
108
+ * same `Pick`/`Partial` shape as before, just re-based on `EvalExpectation` (identical field names),
109
+ * so the answer-check behavior is unchanged.
14
110
  */
15
- export function runDeterministicChecks(answer, evalCase) {
111
+ export function runDeterministicChecks(answer, expectation) {
16
112
  const text = answer.toLowerCase();
17
113
  const failures = [];
18
- for (const needle of evalCase.mustContain) {
114
+ for (const needle of expectation.mustContain) {
19
115
  if (!text.includes(needle.toLowerCase())) {
20
116
  failures.push(`missing "${needle}"`);
21
117
  }
22
118
  }
23
- for (const needle of evalCase.mustNotContain) {
119
+ for (const needle of expectation.mustNotContain) {
24
120
  if (text.includes(needle.toLowerCase())) {
25
121
  failures.push(`forbidden "${needle}"`);
26
122
  }
27
123
  }
28
- if (evalCase.shouldContainAny.length > 0 &&
29
- !evalCase.shouldContainAny.some((needle) => text.includes(needle.toLowerCase()))) {
30
- failures.push(`none of [${evalCase.shouldContainAny.join(' | ')}]`);
124
+ if (expectation.shouldContainAny.length > 0 &&
125
+ !expectation.shouldContainAny.some((needle) => text.includes(needle.toLowerCase()))) {
126
+ failures.push(`none of [${expectation.shouldContainAny.join(' | ')}]`);
127
+ }
128
+ // Use `answer.search(re)` rather than `re.test(answer)`: a stored `RegExp` carrying the `g` flag
129
+ // is stateful via `lastIndex` under `.test()`, whereas `String.prototype.search` ignores it.
130
+ for (const re of expectation.mustMatch ?? []) {
131
+ if (answer.search(re) === -1) {
132
+ failures.push(`answer did not match ${formatRegex(re)}`);
133
+ }
134
+ }
135
+ for (const re of expectation.mustNotMatch ?? []) {
136
+ if (answer.search(re) !== -1) {
137
+ failures.push(`answer matched forbidden ${formatRegex(re)}`);
138
+ }
31
139
  }
140
+ failures.push(...runJsonPathChecks(answer, expectation.jsonPath ?? []));
32
141
  return { passed: failures.length === 0, failures };
33
142
  }
34
143
  //# sourceMappingURL=deterministicChecks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"deterministicChecks.js","sourceRoot":"","sources":["../src/deterministicChecks.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAc,EACd,QAA+E;IAE/E,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC,YAAY,MAAM,GAAG,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,cAAc,EAAE,CAAC;QAC7C,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACxC,QAAQ,CAAC,IAAI,CAAC,cAAc,MAAM,GAAG,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED,IACE,QAAQ,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC;QACpC,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,EAChF,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,YAAY,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACtE,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"deterministicChecks.js","sourceRoot":"","sources":["../src/deterministicChecks.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,22 @@
1
1
  import type { EvalSuiteSummary } from '#src/evalTypes.js';
2
2
  /**
3
- * Write one structured JSON record per case (`<id>.json`) plus one aggregate `results.json`
4
- * (suite totals + every case's verdict/checks/judge/reasons) into `outputDir`. Creates `outputDir`
5
- * (and any missing parents) if it doesn't exist. Mirrors BATCH-1's `writeBatchOutput` (`#src/
6
- * output.js`) — same convention, applied to eval's richer per-case shape.
3
+ * Write one structured JSON record per cell (`<id>.json`, or `<id>__<identity>.json` for a
4
+ * BATCH-12 identity-matrix cell) plus one aggregate `results.json` (suite totals + every cell's
5
+ * verdict/checks/judge/reasons) into `outputDir`. Creates `outputDir` (and any missing parents) if
6
+ * it doesn't exist. Mirrors BATCH-1's `writeBatchOutput` (`#src/output.js`) — same convention,
7
+ * applied to eval's richer per-cell shape.
8
+ *
9
+ * Both the case `id` and the `identity` are validated at parse time to be plain filename-safe tokens
10
+ * (`/^[\w.-]+$/`), so joining them with a `__` separator can neither traverse nor escape `outputDir`.
11
+ * A no-identities cell writes `<id>.json` exactly as before BATCH-12.
12
+ *
13
+ * I1 belt-and-suspenders — because both ids and identity names permit `__`, two DISTINCT authored
14
+ * cells can collapse to the same per-cell filename (e.g. case `x` + identity `y__z` and case `x__y`
15
+ * + identity `z` both → `x__y__z.json`). Dispatch/grading are keyed by the unique `inputIndex` so
16
+ * the run itself is always correct and `results.json` holds every cell, but writing per-cell files
17
+ * by name would silently OVERWRITE one cell's record with another's. Detect that collision up front
18
+ * and throw BEFORE creating the directory or writing anything (→ the eval command's catch → exit 2,
19
+ * a suite-authoring signal) rather than emit a misleading, half-complete set of per-cell files.
7
20
  *
8
21
  * Pure I/O, deliberately separate from {@link ../evalRunner.js}'s `runEvalSuite`: the runner never
9
22
  * touches the filesystem, so unit tests can exercise grading logic without a tmp dir.
@@ -1,18 +1,53 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ /** The per-cell output basename (no extension): `<id>` for a no-identities cell, `<id>__<identity>`
4
+ * for an identity-matrix cell. Both `id` and `identity` are parse-time-validated filename-safe
5
+ * tokens (`/^[\w.-]+$/`), so the `__` join can neither traverse nor escape the output dir. */
6
+ function outputFileBase(result) {
7
+ return result.identity !== undefined ? `${result.id}__${result.identity}` : result.id;
8
+ }
9
+ /** A human label for a cell in diagnostics: `<id>` or `<id> [<identity>]`. */
10
+ function cellLabel(result) {
11
+ return result.identity !== undefined ? `${result.id} [${result.identity}]` : result.id;
12
+ }
3
13
  /**
4
- * Write one structured JSON record per case (`<id>.json`) plus one aggregate `results.json`
5
- * (suite totals + every case's verdict/checks/judge/reasons) into `outputDir`. Creates `outputDir`
6
- * (and any missing parents) if it doesn't exist. Mirrors BATCH-1's `writeBatchOutput` (`#src/
7
- * output.js`) — same convention, applied to eval's richer per-case shape.
14
+ * Write one structured JSON record per cell (`<id>.json`, or `<id>__<identity>.json` for a
15
+ * BATCH-12 identity-matrix cell) plus one aggregate `results.json` (suite totals + every cell's
16
+ * verdict/checks/judge/reasons) into `outputDir`. Creates `outputDir` (and any missing parents) if
17
+ * it doesn't exist. Mirrors BATCH-1's `writeBatchOutput` (`#src/output.js`) — same convention,
18
+ * applied to eval's richer per-cell shape.
19
+ *
20
+ * Both the case `id` and the `identity` are validated at parse time to be plain filename-safe tokens
21
+ * (`/^[\w.-]+$/`), so joining them with a `__` separator can neither traverse nor escape `outputDir`.
22
+ * A no-identities cell writes `<id>.json` exactly as before BATCH-12.
23
+ *
24
+ * I1 belt-and-suspenders — because both ids and identity names permit `__`, two DISTINCT authored
25
+ * cells can collapse to the same per-cell filename (e.g. case `x` + identity `y__z` and case `x__y`
26
+ * + identity `z` both → `x__y__z.json`). Dispatch/grading are keyed by the unique `inputIndex` so
27
+ * the run itself is always correct and `results.json` holds every cell, but writing per-cell files
28
+ * by name would silently OVERWRITE one cell's record with another's. Detect that collision up front
29
+ * and throw BEFORE creating the directory or writing anything (→ the eval command's catch → exit 2,
30
+ * a suite-authoring signal) rather than emit a misleading, half-complete set of per-cell files.
8
31
  *
9
32
  * Pure I/O, deliberately separate from {@link ../evalRunner.js}'s `runEvalSuite`: the runner never
10
33
  * touches the filesystem, so unit tests can exercise grading logic without a tmp dir.
11
34
  */
12
35
  export function writeEvalOutput(outputDir, summary) {
36
+ const seenBy = new Map();
37
+ for (const result of summary.cases) {
38
+ const fileBase = outputFileBase(result);
39
+ const prior = seenBy.get(fileBase);
40
+ if (prior) {
41
+ throw new Error(`eval output filename collision: cells "${cellLabel(prior)}" and "${cellLabel(result)}" ` +
42
+ `both map to "${fileBase}.json". Case ids and identity names both allow "__", so distinct ` +
43
+ '(case × identity) cells can collapse to one filename — rename the case id or identity so ' +
44
+ 'every cell has a unique <id>__<identity> name.');
45
+ }
46
+ seenBy.set(fileBase, result);
47
+ }
13
48
  mkdirSync(outputDir, { recursive: true });
14
49
  for (const result of summary.cases) {
15
- writeFileSync(join(outputDir, `${result.id}.json`), `${JSON.stringify(result, null, 2)}\n`);
50
+ writeFileSync(join(outputDir, `${outputFileBase(result)}.json`), `${JSON.stringify(result, null, 2)}\n`);
16
51
  }
17
52
  writeFileSync(join(outputDir, 'results.json'), `${JSON.stringify(summary, null, 2)}\n`);
18
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"evalOutput.js","sourceRoot":"","sources":["../src/evalOutput.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB,EAAE,OAAyB;IAC1E,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1C,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACnC,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAC9F,CAAC;IAED,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC1F,CAAC"}
1
+ {"version":3,"file":"evalOutput.js","sourceRoot":"","sources":["../src/evalOutput.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC;;8FAE8F;AAC9F,SAAS,cAAc,CAAC,MAAsB;IAC5C,OAAO,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;AACxF,CAAC;AAED,8EAA8E;AAC9E,SAAS,SAAS,CAAC,MAAsB;IACvC,OAAO,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;AACzF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB,EAAE,OAAyB;IAC1E,MAAM,MAAM,GAAG,IAAI,GAAG,EAA0B,CAAC;IACjD,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CACb,0CAA0C,SAAS,CAAC,KAAK,CAAC,UAAU,SAAS,CAAC,MAAM,CAAC,IAAI;gBACvF,gBAAgB,QAAQ,mEAAmE;gBAC3F,2FAA2F;gBAC3F,gDAAgD,CACnD,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC;IAED,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1C,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACnC,aAAa,CACX,IAAI,CAAC,SAAS,EAAE,GAAG,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,EACjD,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CACvC,CAAC;IACJ,CAAC;IAED,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC1F,CAAC"}
@@ -1,25 +1,78 @@
1
1
  import type { RunCellFn } from '#src/types.js';
2
- import type { EvalSuite, EvalSuiteSummary, JudgeFn } from '#src/evalTypes.js';
2
+ import type { EvalSuite, EvalSuiteSummary, JudgeFn, RunConversationFn } from '#src/evalTypes.js';
3
3
  /** Options for {@link runEvalSuite}. */
4
4
  export interface RunEvalSuiteOptions {
5
- /** The injectable per-case run function — same seam as BATCH-1's `RunCellFn`, adapted to send
6
- * each case's `prompt` through the SUT (production wiring: `evalCommand.ts`, reusing
7
- * `batchCommand.ts`'s `buildProductionRunCell`; tests inject a fake). */
8
- runCell: RunCellFn;
9
- /** The injectable judge function. Only consulted for cases that declare a `judgeRubric`;
10
- * omitted entirely = every judge-rubric case fails with a "no judge configured" reason (this
11
- * should not happen in production wiring, where `evalCommand.ts` always supplies one, but the
12
- * runner degrades safely rather than throwing if it's left out). */
5
+ /**
6
+ * The single-run path (suite declares **no** `identities`): one injectable per-case run function,
7
+ * same seam as BATCH-1's `RunCellFn` (production wiring: `evalCommand.ts` via
8
+ * `batchCommand.ts`'s `buildProductionRunCell`; tests inject a fake). Required when the suite has
9
+ * no identities.
10
+ */
11
+ runCell?: RunCellFn;
12
+ /**
13
+ * The matrix path (BATCH-12; suite declares `identities`): one `RunCellFn` per identity, keyed by
14
+ * identity name. Each is built once by the command from `initConfig({ …, identityProfile })` — the
15
+ * same fresh-`.llm` construction `gth batch --models` uses per model — and reused across cases.
16
+ * The runner fans every case over these identities through the SAME execution + grading path the
17
+ * single-run mode uses.
18
+ */
19
+ runCellByIdentity?: Map<string, RunCellFn>;
20
+ /**
21
+ * BATCH-12 Task 2 — the MULTI-TURN seam for the no-identities path: run a whole scripted
22
+ * conversation (a case whose `turns.length > 1`) and return one {@link TurnRunOutcome} per turn.
23
+ * Required only when the suite has multi-turn cases and no identities. Single-turn cases keep
24
+ * using {@link runCell} (the proven `runSingleShot` path, byte-for-byte).
25
+ */
26
+ runConversation?: RunConversationFn;
27
+ /**
28
+ * BATCH-12 Task 2 — the MULTI-TURN seam per identity (matrix path): one {@link RunConversationFn}
29
+ * per identity, each built once by the command from that identity's config, reused across cases.
30
+ * The whole conversation runs once per identity, so per-identity "memory"/authorization is real.
31
+ */
32
+ runConversationByIdentity?: Map<string, RunConversationFn>;
33
+ /** The injectable judge function. Only consulted for expectation blocks that declare a
34
+ * `judgeRubric`; omitted entirely = every judge-rubric block fails with a "no judge configured"
35
+ * reason (the runner degrades safely rather than throwing). The judge is orthogonal to identity:
36
+ * it grades each cell's answer regardless of which identity produced it. */
13
37
  judge?: JudgeFn;
14
- /** Max in-flight cases — reuses BATCH-1's `runBatchMatrix` pool (`DEFAULT_CONCURRENCY` when
38
+ /** Max in-flight cells — reuses BATCH-1's `runBatchMatrix` pool (`DEFAULT_CONCURRENCY` when
15
39
  * omitted); no second concurrency mechanism is introduced for eval. */
16
40
  concurrency?: number;
17
41
  }
18
42
  /**
19
- * Run every case in the suite through the SUT ({@link RunEvalSuiteOptions.runCell}, pooled via
20
- * BATCH-1's `runBatchMatrix`), then grade each answer with deterministic checks and (when the case
21
- * declares a rubric) the judge. A case PASSES iff its deterministic checks pass AND (it has no
22
- * judge rubric OR the judge's rate is at/above the case's `passThreshold`) ported from the field
23
- * user's proven harness semantics (`docs/batch-eval-user-requirements.md` Appendix A).
43
+ * Run every (case × identity) cell of the suite through the SUT ({@link RunCellFn}, pooled via
44
+ * BATCH-1's `runBatchMatrix`), then grade each answer with the applicable expectation blocks'
45
+ * deterministic checks, tool-trace checks, and (when a block declares a rubric) the judge. A cell
46
+ * PASSES iff EVERY applicable block passes ported from the field user's proven harness semantics
47
+ * (`docs/batch-eval-user-requirements.md` Appendix A), generalized to per-identity blocks.
48
+ *
49
+ * ONE concurrency pool: flat and matrix suites both normalize to {@link EvalUnit}s and ride the same
50
+ * `runBatchMatrix` pool; the only divergence is which `RunCellFn` a unit's identity resolves to.
51
+ *
52
+ * BATCH-12 Task 2 — a unit whose case has `turns.length > 1` is a MULTI-TURN conversation: it runs
53
+ * through the injected {@link RunConversationFn} seam (agent/tools built once, messages accumulated
54
+ * across turns) instead of {@link RunCellFn}, still inside the SAME pool, and is graded turn-by-turn
55
+ * by {@link gradeConversationUnit} — the cell PASSES iff EVERY turn's applicable blocks pass. A
56
+ * single-turn unit keeps the proven `runCell` + {@link gradeUnit} path byte-for-byte.
24
57
  */
25
58
  export declare function runEvalSuite(suite: EvalSuite, options: RunEvalSuiteOptions): Promise<EvalSuiteSummary>;
59
+ /** The three-way process exit code for a completed `gth eval` run. See {@link classifyEvalExit}. */
60
+ export type EvalExitCode = 0 | 1 | 2;
61
+ /**
62
+ * BATCH-11 (#405 his #6) — classify a completed suite's {@link EvalSuiteSummary} into a distinct
63
+ * exit code so CI can tell a product regression from a broken harness. BATCH-12 counts matrix
64
+ * CELLS (one per case × identity), not cases:
65
+ *
66
+ * - `0` — every cell passed (unchanged contract).
67
+ * - `1` — the suite **ran** but ≥1 cell FAILED (assertion/judge below threshold). A *product*
68
+ * signal: real, gradeable results, some below the bar.
69
+ * - `2` — **harness error**: no gradeable results at all — an empty suite, or **every** cell's SUT
70
+ * run failed (`sutOk === false`). (The other harness errors that never reach a summary — suite
71
+ * load/parse error, config error, an unresolved identity precondition — are mapped to `2` by the
72
+ * caller (`evalCommand.ts`) in a try/catch.)
73
+ *
74
+ * Classification is anchored on `sutOk`, not the verdict: a cell that ran (`sutOk === true`) but
75
+ * whose judge errored (or whose answer failed a check) is a real result → exit `1`, never `2`. A
76
+ * *mix* of `sutOk:false` and `sutOk:true` cells therefore yields `1`.
77
+ */
78
+ export declare function classifyEvalExit(summary: EvalSuiteSummary): EvalExitCode;