@smthrs/evals 0.0.0-stage → 1.0.0-rc.4
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 +142 -0
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/cjs/Baseline.d.ts +118 -0
- package/dist/cjs/Baseline.d.ts.map +1 -0
- package/dist/cjs/Baseline.js +158 -0
- package/dist/cjs/Baseline.js.map +7 -0
- package/dist/cjs/CaseExecutor.d.ts +85 -0
- package/dist/cjs/CaseExecutor.d.ts.map +1 -0
- package/dist/cjs/CaseExecutor.js +59 -0
- package/dist/cjs/CaseExecutor.js.map +7 -0
- package/dist/cjs/EvalError.d.ts +64 -0
- package/dist/cjs/EvalError.d.ts.map +1 -0
- package/dist/cjs/EvalError.js +54 -0
- package/dist/cjs/EvalError.js.map +7 -0
- package/dist/cjs/Gate.d.ts +55 -0
- package/dist/cjs/Gate.d.ts.map +1 -0
- package/dist/cjs/Gate.js +88 -0
- package/dist/cjs/Gate.js.map +7 -0
- package/dist/cjs/Regression.d.ts +116 -0
- package/dist/cjs/Regression.d.ts.map +1 -0
- package/dist/cjs/Regression.js +191 -0
- package/dist/cjs/Regression.js.map +7 -0
- package/dist/cjs/Report.d.ts +114 -0
- package/dist/cjs/Report.d.ts.map +1 -0
- package/dist/cjs/Report.js +161 -0
- package/dist/cjs/Report.js.map +7 -0
- package/dist/cjs/Runner.d.ts +229 -0
- package/dist/cjs/Runner.d.ts.map +1 -0
- package/dist/cjs/Runner.js +430 -0
- package/dist/cjs/Runner.js.map +7 -0
- package/dist/cjs/Suite.d.ts +139 -0
- package/dist/cjs/Suite.d.ts.map +1 -0
- package/dist/cjs/Suite.js +221 -0
- package/dist/cjs/Suite.js.map +7 -0
- package/dist/cjs/Trials.d.ts +93 -0
- package/dist/cjs/Trials.d.ts.map +1 -0
- package/dist/cjs/Trials.js +82 -0
- package/dist/cjs/Trials.js.map +7 -0
- package/dist/cjs/index.d.ts +76 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +51 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/CanonicalJson.d.ts +44 -0
- package/dist/cjs/internal/CanonicalJson.d.ts.map +1 -0
- package/dist/cjs/internal/CanonicalJson.js +114 -0
- package/dist/cjs/internal/CanonicalJson.js.map +7 -0
- package/dist/cjs/internal/controlCharacters.d.ts +44 -0
- package/dist/cjs/internal/controlCharacters.d.ts.map +1 -0
- package/dist/cjs/internal/controlCharacters.js +42 -0
- package/dist/cjs/internal/controlCharacters.js.map +7 -0
- package/dist/cjs/internal/scorerLabel.d.ts +33 -0
- package/dist/cjs/internal/scorerLabel.d.ts.map +1 -0
- package/dist/cjs/internal/scorerLabel.js +28 -0
- package/dist/cjs/internal/scorerLabel.js.map +7 -0
- package/dist/cjs/internal/tupleKey.d.ts +20 -0
- package/dist/cjs/internal/tupleKey.d.ts.map +1 -0
- package/dist/cjs/internal/tupleKey.js +25 -0
- package/dist/cjs/internal/tupleKey.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/Baseline.d.ts +118 -0
- package/dist/esm/Baseline.d.ts.map +1 -0
- package/dist/esm/Baseline.js +203 -0
- package/dist/esm/Baseline.js.map +1 -0
- package/dist/esm/CaseExecutor.d.ts +85 -0
- package/dist/esm/CaseExecutor.d.ts.map +1 -0
- package/dist/esm/CaseExecutor.js +60 -0
- package/dist/esm/CaseExecutor.js.map +1 -0
- package/dist/esm/EvalError.d.ts +64 -0
- package/dist/esm/EvalError.d.ts.map +1 -0
- package/dist/esm/EvalError.js +65 -0
- package/dist/esm/EvalError.js.map +1 -0
- package/dist/esm/Gate.d.ts +55 -0
- package/dist/esm/Gate.d.ts.map +1 -0
- package/dist/esm/Gate.js +90 -0
- package/dist/esm/Gate.js.map +1 -0
- package/dist/esm/Regression.d.ts +116 -0
- package/dist/esm/Regression.d.ts.map +1 -0
- package/dist/esm/Regression.js +195 -0
- package/dist/esm/Regression.js.map +1 -0
- package/dist/esm/Report.d.ts +114 -0
- package/dist/esm/Report.d.ts.map +1 -0
- package/dist/esm/Report.js +188 -0
- package/dist/esm/Report.js.map +1 -0
- package/dist/esm/Runner.d.ts +229 -0
- package/dist/esm/Runner.d.ts.map +1 -0
- package/dist/esm/Runner.js +430 -0
- package/dist/esm/Runner.js.map +1 -0
- package/dist/esm/Suite.d.ts +139 -0
- package/dist/esm/Suite.d.ts.map +1 -0
- package/dist/esm/Suite.js +255 -0
- package/dist/esm/Suite.js.map +1 -0
- package/dist/esm/Trials.d.ts +93 -0
- package/dist/esm/Trials.d.ts.map +1 -0
- package/dist/esm/Trials.js +116 -0
- package/dist/esm/Trials.js.map +1 -0
- package/dist/esm/index.d.ts +76 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +76 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/CanonicalJson.d.ts +44 -0
- package/dist/esm/internal/CanonicalJson.d.ts.map +1 -0
- package/dist/esm/internal/CanonicalJson.js +202 -0
- package/dist/esm/internal/CanonicalJson.js.map +1 -0
- package/dist/esm/internal/controlCharacters.d.ts +44 -0
- package/dist/esm/internal/controlCharacters.d.ts.map +1 -0
- package/dist/esm/internal/controlCharacters.js +65 -0
- package/dist/esm/internal/controlCharacters.js.map +1 -0
- package/dist/esm/internal/scorerLabel.d.ts +33 -0
- package/dist/esm/internal/scorerLabel.d.ts.map +1 -0
- package/dist/esm/internal/scorerLabel.js +33 -0
- package/dist/esm/internal/scorerLabel.js.map +1 -0
- package/dist/esm/internal/tupleKey.d.ts +20 -0
- package/dist/esm/internal/tupleKey.d.ts.map +1 -0
- package/dist/esm/internal/tupleKey.js +20 -0
- package/dist/esm/internal/tupleKey.js.map +1 -0
- package/docs/README.md +130 -0
- package/docs/api.md +626 -0
- package/docs/concepts/determinism.md +64 -0
- package/docs/concepts/fixed-suites.md +71 -0
- package/docs/concepts/step-keys.md +60 -0
- package/docs/guides/author-a-suite.md +115 -0
- package/docs/guides/gate-a-run-in-ci.md +129 -0
- package/docs/guides/maintain-a-baseline.md +91 -0
- package/docs/guides/run-a-suite.md +150 -0
- package/docs/installation.md +46 -0
- package/docs/quickstart.md +238 -0
- package/docs/troubleshooting.md +148 -0
- package/package.json +198 -3
- package/src/Baseline.ts +259 -0
- package/src/CaseExecutor.ts +104 -0
- package/src/EvalError.ts +74 -0
- package/src/Gate.ts +128 -0
- package/src/Regression.ts +314 -0
- package/src/Report.ts +268 -0
- package/src/Runner.ts +759 -0
- package/src/Suite.ts +358 -0
- package/src/Trials.ts +154 -0
- package/src/index.ts +76 -0
- package/src/internal/CanonicalJson.ts +234 -0
- package/src/internal/controlCharacters.ts +66 -0
- package/src/internal/scorerLabel.ts +35 -0
- package/src/internal/tupleKey.ts +20 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Installation"
|
|
3
|
+
description: "Add @smthrs/evals to a workspace package, plus its runtime requirements and entry points."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The package is at 1.0.0-rc.0 and is not on the npm registry yet. It is a
|
|
7
|
+
workspace package of https://github.com/smithersai/smithers, so use it from a
|
|
8
|
+
package in a clone of that repository:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
git clone https://github.com/smithersai/smithers
|
|
12
|
+
cd smithers
|
|
13
|
+
pnpm install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then depend on it through the workspace protocol in your package's
|
|
17
|
+
`package.json` and run `pnpm install` again:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@smthrs/evals": "workspace:*"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Runtime requirements
|
|
28
|
+
|
|
29
|
+
- Node.js 26.4.0 or later, from the package's `engines` field.
|
|
30
|
+
- `effect` 4.0.0-rc.115. Suites, runs, baselines, and gates are all `Effect`
|
|
31
|
+
values, so every program composes with the `effect` library directly.
|
|
32
|
+
- `@smthrs/core` supplies `Flow` values. `@smthrs/scorers` supplies scorers,
|
|
33
|
+
bindings, and the pure `@smthrs/scorers/ScoreGate` grading contract, including
|
|
34
|
+
`ScoreGateError`. The evaluation runtime does not load `@smthrs/testing`;
|
|
35
|
+
that package supplies a test facade for development consumers. To run the
|
|
36
|
+
agent behind a case, add [@smthrs/agent](/api/agent) as well.
|
|
37
|
+
|
|
38
|
+
## Entry points
|
|
39
|
+
|
|
40
|
+
- `@smthrs/evals` exports the nine namespaces: `EvalError`, `Suite`,
|
|
41
|
+
`CaseExecutor`, `Runner`, `Baseline`, `Regression`, `Report`, `Gate`, and
|
|
42
|
+
`Trials`.
|
|
43
|
+
- `@smthrs/evals/<Module>` imports one namespace directly, for example
|
|
44
|
+
`@smthrs/evals/Suite`.
|
|
45
|
+
- `@smthrs/evals/package.json` is exported. The `internal/*` modules and
|
|
46
|
+
nested `*/index` subpaths are not public.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Quickstart"
|
|
3
|
+
description: "Declare a fixed suite, run it, record a baseline, and gate the next run against it."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
This tutorial walks one complete evaluation loop. You declare a suite for a
|
|
7
|
+
greeting flow, run it, record the scores as a baseline, and gate a later run
|
|
8
|
+
against that baseline. By the end you have a script that exits 0 while the
|
|
9
|
+
flow behaves and 1 when a score drops.
|
|
10
|
+
|
|
11
|
+
You need a workspace package with `@smthrs/evals` installed. For setup, see
|
|
12
|
+
[Installation](./installation.md).
|
|
13
|
+
|
|
14
|
+
## 1. Declare a target and a scorer
|
|
15
|
+
|
|
16
|
+
A target is the flow the suite exercises. A scorer grades one execution and
|
|
17
|
+
returns a number in [0, 1]. Both come from packages the pipeline composes:
|
|
18
|
+
`Flow` from [@smthrs/core](/api/core) and `Scorer` from
|
|
19
|
+
[@smthrs/scorers](/api/scorers).
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { Scorer } from "@smthrs/scorers"
|
|
23
|
+
import { Effect } from "effect"
|
|
24
|
+
|
|
25
|
+
const greet = { name: "greet" }
|
|
26
|
+
|
|
27
|
+
const polite = Scorer.make({
|
|
28
|
+
id: "quickstart/polite",
|
|
29
|
+
version: "1",
|
|
30
|
+
name: "polite",
|
|
31
|
+
score: ({ output }) => Effect.succeed({ score: String(output).startsWith("Hello") ? 1 : 0 })
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 2. Declare the suite
|
|
36
|
+
|
|
37
|
+
A suite is the fixed half of the evaluation: named cases, the scorer bindings
|
|
38
|
+
that grade them, and the concurrency the runner is allowed. `Suite.make`
|
|
39
|
+
validates the declaration and snapshots it, so the suite cannot change after
|
|
40
|
+
validation.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { Suite } from "@smthrs/evals"
|
|
44
|
+
import { Binding } from "@smthrs/scorers"
|
|
45
|
+
|
|
46
|
+
Effect.gen(function*() {
|
|
47
|
+
const suite = yield* Suite.make({
|
|
48
|
+
name: "greetings",
|
|
49
|
+
cases: [
|
|
50
|
+
{ name: "ada", input: { name: "Ada" }, expected: "Hello, Ada" },
|
|
51
|
+
{ name: "grace", input: { name: "Grace" }, expected: "Hello, Grace" }
|
|
52
|
+
],
|
|
53
|
+
bindings: [Binding.make({ scorer: polite, appliesTo: greet })],
|
|
54
|
+
concurrency: 2
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`Binding.make` attaches the `polite` scorer to the `greet` flow. The match is
|
|
60
|
+
by reference identity: only an execution that reports the `greet` value itself
|
|
61
|
+
as its target is graded by this binding.
|
|
62
|
+
|
|
63
|
+
## 3. Execute cases through an injected boundary
|
|
64
|
+
|
|
65
|
+
The `CaseExecutor` service says how one case runs against the target. Each
|
|
66
|
+
execution reports its output, a step key identifying the work, a latency, and
|
|
67
|
+
the target flow value itself.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { CaseExecutor } from "@smthrs/evals"
|
|
71
|
+
|
|
72
|
+
const executor = CaseExecutor.make((suiteCase) =>
|
|
73
|
+
Effect.succeed({
|
|
74
|
+
output: `Hello, ${(suiteCase.input as { readonly name: string }).name}`,
|
|
75
|
+
stepKey: suiteCase.name,
|
|
76
|
+
latencyMs: 0,
|
|
77
|
+
target: greet
|
|
78
|
+
})
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Keep the step key fixed per case. The comparison reads a changed key as
|
|
83
|
+
different work and a moved score under an unchanged key as nondeterminism, and
|
|
84
|
+
both readings need the key stated, not derived from the run. For the full
|
|
85
|
+
rule, see [Step keys and comparison](./concepts/step-keys.md).
|
|
86
|
+
|
|
87
|
+
## 4. Run the suite
|
|
88
|
+
|
|
89
|
+
`Runner.run` executes the cases at the suite's concurrency and scores every
|
|
90
|
+
execution whose target matches a binding. It takes `runId` and `at` from you,
|
|
91
|
+
so a re-run over the same inputs produces identical observations.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { Runner } from "@smthrs/evals"
|
|
95
|
+
|
|
96
|
+
Effect.gen(function*() {
|
|
97
|
+
const run = yield* Runner.run(suite, {
|
|
98
|
+
runId: "quickstart-1",
|
|
99
|
+
at: "2026-01-01T00:00:00.000Z"
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `at` value must be a canonical UTC timestamp with millisecond precision,
|
|
105
|
+
such as `2026-01-01T00:00:00.000Z`. `new Date().toISOString()` satisfies the
|
|
106
|
+
format.
|
|
107
|
+
|
|
108
|
+
## 5. Record the baseline
|
|
109
|
+
|
|
110
|
+
`Baseline.fromRun` keeps the run's successful scores, and `Baseline.write`
|
|
111
|
+
serializes them as canonical JSON to commit beside the suite.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { Baseline, EvalError } from "@smthrs/evals"
|
|
115
|
+
import { writeFile } from "node:fs/promises"
|
|
116
|
+
|
|
117
|
+
Effect.gen(function*() {
|
|
118
|
+
const failures = run.cases.filter((result) => result.error !== undefined)
|
|
119
|
+
if (failures.length > 0) {
|
|
120
|
+
return yield* Effect.fail(
|
|
121
|
+
new EvalError.EvalError({
|
|
122
|
+
code: "executor",
|
|
123
|
+
message: `Refusing to record a baseline: ${failures.length} case(s) did not finish`
|
|
124
|
+
})
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
const baseline = yield* Baseline.fromRun(run)
|
|
128
|
+
yield* Effect.promise(() => writeFile("baseline.json", Baseline.write(baseline)))
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Inconclusive observations are dropped: a baseline records what was measured,
|
|
133
|
+
and an inconclusive observation measured nothing.
|
|
134
|
+
|
|
135
|
+
## 6. Gate the next run against the baseline
|
|
136
|
+
|
|
137
|
+
On the next run, load the committed artifact, compare it against the run,
|
|
138
|
+
render the report, and grade the comparison.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { Gate, Regression, Report } from "@smthrs/evals"
|
|
142
|
+
import { readFile } from "node:fs/promises"
|
|
143
|
+
|
|
144
|
+
Effect.gen(function*() {
|
|
145
|
+
const committed = yield* Baseline.load(
|
|
146
|
+
yield* Effect.promise(() => readFile("baseline.json", "utf8"))
|
|
147
|
+
)
|
|
148
|
+
const comparison = yield* Regression.compare(committed, run)
|
|
149
|
+
yield* Effect.sync(() => process.stdout.write(Report.markdown(comparison)))
|
|
150
|
+
const verdict = yield* Gate.check(comparison, { mean: 1 })
|
|
151
|
+
const grade = Gate.ciGrade(verdict)
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 7. Put it together
|
|
156
|
+
|
|
157
|
+
The complete script, `quickstart.ts`:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { Baseline, CaseExecutor, EvalError, Gate, Regression, Report, Runner, Suite } from "@smthrs/evals"
|
|
161
|
+
import { Binding, Scorer } from "@smthrs/scorers"
|
|
162
|
+
import { Effect, Layer } from "effect"
|
|
163
|
+
import { readFile, writeFile } from "node:fs/promises"
|
|
164
|
+
|
|
165
|
+
const greet = { name: "greet" }
|
|
166
|
+
|
|
167
|
+
const polite = Scorer.make({
|
|
168
|
+
id: "quickstart/polite",
|
|
169
|
+
version: "1",
|
|
170
|
+
name: "polite",
|
|
171
|
+
score: ({ output }) => Effect.succeed({ score: String(output).startsWith("Hello") ? 1 : 0 })
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const executor = CaseExecutor.make((suiteCase) =>
|
|
175
|
+
Effect.succeed({
|
|
176
|
+
output: `Hello, ${(suiteCase.input as { readonly name: string }).name}`,
|
|
177
|
+
stepKey: suiteCase.name,
|
|
178
|
+
latencyMs: 0,
|
|
179
|
+
target: greet
|
|
180
|
+
})
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const program = Effect.gen(function*() {
|
|
184
|
+
const suite = yield* Suite.make({
|
|
185
|
+
name: "greetings",
|
|
186
|
+
cases: [
|
|
187
|
+
{ name: "ada", input: { name: "Ada" }, expected: "Hello, Ada" },
|
|
188
|
+
{ name: "grace", input: { name: "Grace" }, expected: "Hello, Grace" }
|
|
189
|
+
],
|
|
190
|
+
bindings: [Binding.make({ scorer: polite, appliesTo: greet })],
|
|
191
|
+
concurrency: 2
|
|
192
|
+
})
|
|
193
|
+
const run = yield* Runner.run(suite, {
|
|
194
|
+
runId: "quickstart-1",
|
|
195
|
+
at: "2026-01-01T00:00:00.000Z"
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if (process.argv.includes("--update")) {
|
|
199
|
+
const failures = run.cases.filter((result) => result.error !== undefined)
|
|
200
|
+
if (failures.length > 0) {
|
|
201
|
+
return yield* Effect.fail(
|
|
202
|
+
new EvalError.EvalError({
|
|
203
|
+
code: "executor",
|
|
204
|
+
message: `Refusing to record a baseline: ${failures.length} case(s) did not finish`
|
|
205
|
+
})
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
const baseline = yield* Baseline.fromRun(run)
|
|
209
|
+
yield* Effect.promise(() => writeFile("baseline.json", Baseline.write(baseline)))
|
|
210
|
+
yield* Effect.sync(() => process.stdout.write("baseline recorded\n"))
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const committed = yield* Baseline.load(
|
|
215
|
+
yield* Effect.promise(() => readFile("baseline.json", "utf8"))
|
|
216
|
+
)
|
|
217
|
+
const comparison = yield* Regression.compare(committed, run)
|
|
218
|
+
yield* Effect.sync(() => process.stdout.write(Report.markdown(comparison)))
|
|
219
|
+
const verdict = yield* Gate.check(comparison, { mean: 1 })
|
|
220
|
+
const { exitCode, summary } = Gate.ciGrade(verdict)
|
|
221
|
+
yield* Effect.sync(() => process.stdout.write(`${summary}\n`))
|
|
222
|
+
return exitCode
|
|
223
|
+
}).pipe(Effect.provide(Layer.succeed(CaseExecutor.CaseExecutor)(executor)))
|
|
224
|
+
|
|
225
|
+
process.exitCode = await Effect.runPromise(program)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
1. Record the baseline with `node quickstart.ts --update`. The script prints
|
|
229
|
+
`baseline recorded` and exits 0. If any case failed, it refuses to record
|
|
230
|
+
and fails with an `executor` `EvalError` instead.
|
|
231
|
+
2. Gate against it with `node quickstart.ts`. The script prints the Markdown
|
|
232
|
+
report, then `passed`, and exits 0.
|
|
233
|
+
3. Change the executor to answer `Hi` instead of `Hello` and gate again. Both
|
|
234
|
+
cases score 0 at their unchanged step keys, the comparison reports
|
|
235
|
+
nondeterminism, and the script exits 1.
|
|
236
|
+
|
|
237
|
+
From here: [author a larger suite](./guides/author-a-suite.md), or read how
|
|
238
|
+
[step keys decide what a comparison reports](./concepts/step-keys.md).
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Troubleshooting"
|
|
3
|
+
description: "Every failure code @smthrs/evals raises, what it means, and who fixes it."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Failures arrive as `EvalError` values with a stable `code`, a `message`
|
|
7
|
+
written for a CI log, and usually a `path` locating the offending value. The
|
|
8
|
+
`code` is the branch point, and each code names one owner for the fix.
|
|
9
|
+
|
|
10
|
+
## invalid_suite
|
|
11
|
+
|
|
12
|
+
The suite declaration is wrong. The suite author fixes it.
|
|
13
|
+
|
|
14
|
+
| Symptom in the message | Path | Cause |
|
|
15
|
+
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
16
|
+
| `Suite name must not be empty`, or names a control character | `name` | The suite name is blank or holds a control character |
|
|
17
|
+
| `Suite must contain at least one case` | `cases` | The `cases` array is empty |
|
|
18
|
+
| `Suite must contain at most 10000 cases` | `cases` | The suite exceeds `Suite.limits.cases` |
|
|
19
|
+
| `Suite concurrency must be a positive safe integer` | `concurrency` | Zero, negative, fractional, or `NaN` concurrency |
|
|
20
|
+
| `Suite concurrency must be at most 1024` | `concurrency` | The suite exceeds `Suite.limits.concurrency` |
|
|
21
|
+
| `Suite case name must not be empty`, or names a control character | `cases[N].name` | A case name is blank or holds a control character |
|
|
22
|
+
| `Duplicate suite case` | `cases[N].name` | Two cases share a name |
|
|
23
|
+
| `Suite data must be structured-cloneable` | `cases[N].input`, `cases[N].expected`, `bindings[N].sampling`, `bindings[N].groundTruth`, `bindings[N].context` | The value holds a function, a class instance, or anything else `structuredClone` rejects |
|
|
24
|
+
| `JSON Lines fixture must be at most 8388608 characters` | `text` | The fixture exceeds `Suite.limits.fixtureLength`; it was rejected before parsing |
|
|
25
|
+
| `Invalid JSON on line N` | `line[N]` | The 1-based fixture line is not JSON |
|
|
26
|
+
| `Invalid suite case on line N` | `line[N]` | The fixture line is not a `{ name, input, expected? }` case |
|
|
27
|
+
| `Invalid sampling policy for scorer` | `bindings[N].sampling` | The scorers package cannot decide the policy; see the [scorers API](/api/scorers) |
|
|
28
|
+
|
|
29
|
+
## invalid_run_options
|
|
30
|
+
|
|
31
|
+
`Runner.run`'s own options are wrong, not the suite. The caller fixes them.
|
|
32
|
+
|
|
33
|
+
- `Deterministic runs require a non-empty runId`, at `options.runId`: pass a
|
|
34
|
+
run identity.
|
|
35
|
+
- `Deterministic runs require a canonical UTC timestamp such as
|
|
36
|
+
2026-01-01T00:00:00.000Z`, at `options.at`: the value must carry millisecond
|
|
37
|
+
precision and round-trip through parse and re-render.
|
|
38
|
+
`new Date().toISOString()` satisfies the format.
|
|
39
|
+
|
|
40
|
+
## invalid_baseline
|
|
41
|
+
|
|
42
|
+
The committed baseline is unreadable or belongs to another suite. Check the
|
|
43
|
+
artifact and its ownership before regenerating it from a green run.
|
|
44
|
+
|
|
45
|
+
- `Baseline is not valid JSON` or `Baseline must be an object`: the file is
|
|
46
|
+
not a baseline artifact. These two carry no `path`.
|
|
47
|
+
- `Baseline version must be 1`, at `version`: the artifact was written by a
|
|
48
|
+
different version of the format.
|
|
49
|
+
- `Baseline must be at most 16777216 characters`, at `text`: the file exceeds
|
|
50
|
+
`Baseline.limits.length` and was rejected before parsing.
|
|
51
|
+
- `Baseline field 'suite' must be a string`, at `suite`: a present top-level
|
|
52
|
+
`suite` has the wrong type.
|
|
53
|
+
- `Baseline field 'suite' must not contain the control character U+XXXX`, at
|
|
54
|
+
`suite`: the top-level `suite` holds a control character.
|
|
55
|
+
- `Cannot infer baseline suite: legacy artifact has no records`, at `suite`:
|
|
56
|
+
the version-1 artifact has neither a top-level `suite` nor records. Add the
|
|
57
|
+
known owning suite explicitly, or regenerate it from that suite's green run.
|
|
58
|
+
- `Cannot infer baseline suite: legacy records name multiple suites`, at
|
|
59
|
+
`suite`: records disagree on ownership. Recover a baseline for the intended
|
|
60
|
+
suite; do not choose the first record's suite for a mixed artifact.
|
|
61
|
+
- `Baseline records must be an array`, at `records`.
|
|
62
|
+
- A record that is not an object, an identity field that is not a string or
|
|
63
|
+
holds a control character, or a score outside [0, 1]: the record index and
|
|
64
|
+
field are in `path`, as in `records[3].score`.
|
|
65
|
+
- `Baseline belongs to suite '<name>', but the run is suite '<name>'`, at
|
|
66
|
+
`baseline.suite` or `baseline.records`: comparison loaded the wrong file.
|
|
67
|
+
Check the path CI reads.
|
|
68
|
+
|
|
69
|
+
A suite-less version-1 artifact loads when its records are nonempty and all
|
|
70
|
+
name the same suite. `Baseline.write` of that loaded value adds the inferred
|
|
71
|
+
top-level `suite` and retains the existing scores.
|
|
72
|
+
|
|
73
|
+
## invalid_tolerance
|
|
74
|
+
|
|
75
|
+
A comparison tolerance is not a finite non-negative number, at
|
|
76
|
+
`tolerances.absolute` or `tolerances.relative`. Fix the call, not the
|
|
77
|
+
baseline.
|
|
78
|
+
|
|
79
|
+
## invalid_trials
|
|
80
|
+
|
|
81
|
+
A `Trials` call received counts outside their bounds: a run count that is not
|
|
82
|
+
a positive integer, a pass count outside `0..n`, or a `k` outside `1..n`. The
|
|
83
|
+
`path` names the argument (`n`, `c`, `k`, or `cases['<name>']` for a case with
|
|
84
|
+
no runs). Fix the call; `Trials.aggregate` already clamps `k` for a short case.
|
|
85
|
+
|
|
86
|
+
## executor
|
|
87
|
+
|
|
88
|
+
The target flow failed for a case, or no executor was available.
|
|
89
|
+
|
|
90
|
+
- `Target failed for case '<name>': ...`: the executor failed. The run keeps
|
|
91
|
+
the typed error, locates it with the executor's own `path` or with
|
|
92
|
+
`cases['<name>']`, and produces no observations for the case. The rest of
|
|
93
|
+
the suite still ran. Read the retained `cause`.
|
|
94
|
+
- `No executor is available for case '<name>'`: the run was provided
|
|
95
|
+
`CaseExecutor.makeNoop()` or `CaseExecutor.layerNoop`. Wire a real executor.
|
|
96
|
+
|
|
97
|
+
## ambiguous_score_job
|
|
98
|
+
|
|
99
|
+
Two score jobs share a step key and a scorer, so an order-only batch runner
|
|
100
|
+
cannot attribute their results to a case. Give each case its own step key, or
|
|
101
|
+
provide a batch runner that implements `runBatchCorrelated`.
|
|
102
|
+
|
|
103
|
+
## scorer_protocol
|
|
104
|
+
|
|
105
|
+
A batch runner broke the `runBatch` or `runBatchCorrelated` contract: the
|
|
106
|
+
wrong number of results, a duplicate identity, an unknown identity, or an
|
|
107
|
+
observation that does not echo its job's `targetStepKey` and `scorerKey`.
|
|
108
|
+
Nothing it returned can be trusted. The message names the offending call, as
|
|
109
|
+
in `runBatchCorrelated[0]`. Compare the adapter against `Runner.makeInline()`,
|
|
110
|
+
the reference implementation of the protocol.
|
|
111
|
+
|
|
112
|
+
## scorer_unavailable
|
|
113
|
+
|
|
114
|
+
No batch runner was available to score with. This is `Runner.layerNoop`
|
|
115
|
+
working as declared: every bound score becomes an inconclusive observation,
|
|
116
|
+
and a gate grades the run undecidable rather than red. When the suite should
|
|
117
|
+
score, provide `Runner.layerInline` or a real adapter.
|
|
118
|
+
|
|
119
|
+
## Failures that are not EvalError
|
|
120
|
+
|
|
121
|
+
Two wiring mistakes fail outside the `EvalError` channel:
|
|
122
|
+
|
|
123
|
+
- `CaseExecutor.make` with no callback throws a `TypeError` synchronously.
|
|
124
|
+
Pass a function, or an object with a `run` or `execute` callback.
|
|
125
|
+
- `Gate.check` with a threshold outside [0, 1] fails with a `ScoreGateError`,
|
|
126
|
+
code `invalid_threshold`, from [@smthrs/scorers/ScoreGate](/api/scorers). Catch it in
|
|
127
|
+
the error channel and report it.
|
|
128
|
+
|
|
129
|
+
## Symptoms
|
|
130
|
+
|
|
131
|
+
- Every score is inconclusive. Read the observation's `reason`. A
|
|
132
|
+
`Scorer execution was inconclusive: ...` reason names a scorer or batch
|
|
133
|
+
runner that failed. `<uncoercible cause>` means its failure could not be
|
|
134
|
+
converted to text. Failure reasons use the scorers package's 1024-byte limit.
|
|
135
|
+
A reason about a score outside [0, 1] or an unusable observation kind names a
|
|
136
|
+
scorer that returned a bad result. A suite provided `Runner.layerNoop` also
|
|
137
|
+
lands here.
|
|
138
|
+
- A binding graded nothing. The execution's `target` is not the exact flow
|
|
139
|
+
value the binding's `appliesTo` holds (reference identity, not structure),
|
|
140
|
+
the case failed before scoring, or the sampling policy decided against the
|
|
141
|
+
execution.
|
|
142
|
+
- The gate exits 5. The gate could not decide, or it passed with unresolved
|
|
143
|
+
observations. The Markdown report's Case failures, Missing observations, and
|
|
144
|
+
Inconclusive sections name what is missing. Exit 5 is an unusable harness to
|
|
145
|
+
repair, not a result to read.
|
|
146
|
+
- The report printed a secret. `Report.json` embeds each case's raw
|
|
147
|
+
`execution.output` and redacts nothing. Keep the report out of readable logs
|
|
148
|
+
when cases carry secrets.
|
package/package.json
CHANGED
|
@@ -1,6 +1,201 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smthrs/evals",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0-rc.4",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"smthrs": {
|
|
7
|
+
"group": "agent"
|
|
8
|
+
},
|
|
9
|
+
"description": "Fixed-suite evaluation, baselines, regression reports, and score gates for flows",
|
|
10
|
+
"homepage": "https://github.com/smithersai/smithers",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/smithersai/smithers.git",
|
|
14
|
+
"directory": "packages/smithers/agent/evals"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/smithersai/smithers/issues"
|
|
18
|
+
},
|
|
19
|
+
"tags": [
|
|
20
|
+
"typescript",
|
|
21
|
+
"effect",
|
|
22
|
+
"flow",
|
|
23
|
+
"evaluation"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"typescript",
|
|
27
|
+
"effect",
|
|
28
|
+
"flow",
|
|
29
|
+
"evaluation"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=26.4.0"
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": [],
|
|
35
|
+
"exports": {
|
|
36
|
+
"./package.json": "./package.json",
|
|
37
|
+
".": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/esm/index.d.ts",
|
|
40
|
+
"default": "./dist/esm/index.js"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/cjs/index.d.ts",
|
|
44
|
+
"default": "./dist/cjs/index.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"./internal/*": null,
|
|
48
|
+
"./*/index": null,
|
|
49
|
+
"./Baseline": {
|
|
50
|
+
"import": {
|
|
51
|
+
"types": "./dist/esm/Baseline.d.ts",
|
|
52
|
+
"default": "./dist/esm/Baseline.js"
|
|
53
|
+
},
|
|
54
|
+
"require": {
|
|
55
|
+
"types": "./dist/cjs/Baseline.d.ts",
|
|
56
|
+
"default": "./dist/cjs/Baseline.js"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"./CaseExecutor": {
|
|
60
|
+
"import": {
|
|
61
|
+
"types": "./dist/esm/CaseExecutor.d.ts",
|
|
62
|
+
"default": "./dist/esm/CaseExecutor.js"
|
|
63
|
+
},
|
|
64
|
+
"require": {
|
|
65
|
+
"types": "./dist/cjs/CaseExecutor.d.ts",
|
|
66
|
+
"default": "./dist/cjs/CaseExecutor.js"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"./EvalError": {
|
|
70
|
+
"import": {
|
|
71
|
+
"types": "./dist/esm/EvalError.d.ts",
|
|
72
|
+
"default": "./dist/esm/EvalError.js"
|
|
73
|
+
},
|
|
74
|
+
"require": {
|
|
75
|
+
"types": "./dist/cjs/EvalError.d.ts",
|
|
76
|
+
"default": "./dist/cjs/EvalError.js"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"./Gate": {
|
|
80
|
+
"import": {
|
|
81
|
+
"types": "./dist/esm/Gate.d.ts",
|
|
82
|
+
"default": "./dist/esm/Gate.js"
|
|
83
|
+
},
|
|
84
|
+
"require": {
|
|
85
|
+
"types": "./dist/cjs/Gate.d.ts",
|
|
86
|
+
"default": "./dist/cjs/Gate.js"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"./Regression": {
|
|
90
|
+
"import": {
|
|
91
|
+
"types": "./dist/esm/Regression.d.ts",
|
|
92
|
+
"default": "./dist/esm/Regression.js"
|
|
93
|
+
},
|
|
94
|
+
"require": {
|
|
95
|
+
"types": "./dist/cjs/Regression.d.ts",
|
|
96
|
+
"default": "./dist/cjs/Regression.js"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"./Report": {
|
|
100
|
+
"import": {
|
|
101
|
+
"types": "./dist/esm/Report.d.ts",
|
|
102
|
+
"default": "./dist/esm/Report.js"
|
|
103
|
+
},
|
|
104
|
+
"require": {
|
|
105
|
+
"types": "./dist/cjs/Report.d.ts",
|
|
106
|
+
"default": "./dist/cjs/Report.js"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"./Runner": {
|
|
110
|
+
"import": {
|
|
111
|
+
"types": "./dist/esm/Runner.d.ts",
|
|
112
|
+
"default": "./dist/esm/Runner.js"
|
|
113
|
+
},
|
|
114
|
+
"require": {
|
|
115
|
+
"types": "./dist/cjs/Runner.d.ts",
|
|
116
|
+
"default": "./dist/cjs/Runner.js"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"./Suite": {
|
|
120
|
+
"import": {
|
|
121
|
+
"types": "./dist/esm/Suite.d.ts",
|
|
122
|
+
"default": "./dist/esm/Suite.js"
|
|
123
|
+
},
|
|
124
|
+
"require": {
|
|
125
|
+
"types": "./dist/cjs/Suite.d.ts",
|
|
126
|
+
"default": "./dist/cjs/Suite.js"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"./Trials": {
|
|
130
|
+
"import": {
|
|
131
|
+
"types": "./dist/esm/Trials.d.ts",
|
|
132
|
+
"default": "./dist/esm/Trials.js"
|
|
133
|
+
},
|
|
134
|
+
"require": {
|
|
135
|
+
"types": "./dist/cjs/Trials.d.ts",
|
|
136
|
+
"default": "./dist/cjs/Trials.js"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"./index": {
|
|
140
|
+
"import": {
|
|
141
|
+
"types": "./dist/esm/index.d.ts",
|
|
142
|
+
"default": "./dist/esm/index.js"
|
|
143
|
+
},
|
|
144
|
+
"require": {
|
|
145
|
+
"types": "./dist/cjs/index.d.ts",
|
|
146
|
+
"default": "./dist/cjs/index.js"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"files": [
|
|
151
|
+
"src/**/*.ts",
|
|
152
|
+
"dist/**/*.js",
|
|
153
|
+
"dist/**/*.js.map",
|
|
154
|
+
"dist/**/*.d.ts",
|
|
155
|
+
"dist/**/*.d.ts.map",
|
|
156
|
+
"dist/**/package.json",
|
|
157
|
+
"LICENSE",
|
|
158
|
+
"README.md",
|
|
159
|
+
"CHANGELOG.md",
|
|
160
|
+
"docs/"
|
|
161
|
+
],
|
|
162
|
+
"publishConfig": {
|
|
163
|
+
"access": "public",
|
|
164
|
+
"provenance": true,
|
|
165
|
+
"tag": "next"
|
|
166
|
+
},
|
|
167
|
+
"dependencies": {
|
|
168
|
+
"@smthrs/core": "1.0.0-rc.4",
|
|
169
|
+
"@smthrs/scorers": "1.0.0-rc.4"
|
|
170
|
+
},
|
|
171
|
+
"peerDependencies": {
|
|
172
|
+
"effect": "4.0.0-rc.115"
|
|
173
|
+
},
|
|
174
|
+
"devDependencies": {
|
|
175
|
+
"@effect/language-service": "0.87.2",
|
|
176
|
+
"@eslint/js": "9.39.5",
|
|
177
|
+
"@types/node": "26.4.1",
|
|
178
|
+
"@vitest/coverage-v8": "5.0.0",
|
|
179
|
+
"dprint": "0.57.1",
|
|
180
|
+
"effect": "4.0.0-rc.115",
|
|
181
|
+
"esbuild": "0.28.2",
|
|
182
|
+
"eslint": "9.39.5",
|
|
183
|
+
"eslint-import-resolver-typescript": "4.4.5",
|
|
184
|
+
"eslint-plugin-import": "2.32.0",
|
|
185
|
+
"eslint-plugin-jsdoc": "64.3.4",
|
|
186
|
+
"eslint-plugin-unicorn": "65.0.0",
|
|
187
|
+
"madge": "8.0.0",
|
|
188
|
+
"typescript": "5.9.3",
|
|
189
|
+
"typescript-eslint": "8.69.0",
|
|
190
|
+
"vitest": "5.0.0"
|
|
191
|
+
},
|
|
192
|
+
"scripts": {
|
|
193
|
+
"lint": "eslint src --max-warnings=0 && dprint check",
|
|
194
|
+
"format": "dprint fmt",
|
|
195
|
+
"build": "node scripts/build.mjs",
|
|
196
|
+
"check": "tsc -b tsconfig.json && tsc -p tsconfig.test.json --noEmit",
|
|
197
|
+
"circular": "node scripts/circular.mjs",
|
|
198
|
+
"test": "vitest",
|
|
199
|
+
"coverage": "vitest --coverage"
|
|
200
|
+
}
|
|
6
201
|
}
|