@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +62 -2
  4. package/dist/cjs/Baseline.d.ts +118 -0
  5. package/dist/cjs/Baseline.d.ts.map +1 -0
  6. package/dist/cjs/Baseline.js +158 -0
  7. package/dist/cjs/Baseline.js.map +7 -0
  8. package/dist/cjs/CaseExecutor.d.ts +85 -0
  9. package/dist/cjs/CaseExecutor.d.ts.map +1 -0
  10. package/dist/cjs/CaseExecutor.js +59 -0
  11. package/dist/cjs/CaseExecutor.js.map +7 -0
  12. package/dist/cjs/EvalError.d.ts +64 -0
  13. package/dist/cjs/EvalError.d.ts.map +1 -0
  14. package/dist/cjs/EvalError.js +54 -0
  15. package/dist/cjs/EvalError.js.map +7 -0
  16. package/dist/cjs/Gate.d.ts +55 -0
  17. package/dist/cjs/Gate.d.ts.map +1 -0
  18. package/dist/cjs/Gate.js +88 -0
  19. package/dist/cjs/Gate.js.map +7 -0
  20. package/dist/cjs/Regression.d.ts +116 -0
  21. package/dist/cjs/Regression.d.ts.map +1 -0
  22. package/dist/cjs/Regression.js +191 -0
  23. package/dist/cjs/Regression.js.map +7 -0
  24. package/dist/cjs/Report.d.ts +114 -0
  25. package/dist/cjs/Report.d.ts.map +1 -0
  26. package/dist/cjs/Report.js +161 -0
  27. package/dist/cjs/Report.js.map +7 -0
  28. package/dist/cjs/Runner.d.ts +229 -0
  29. package/dist/cjs/Runner.d.ts.map +1 -0
  30. package/dist/cjs/Runner.js +430 -0
  31. package/dist/cjs/Runner.js.map +7 -0
  32. package/dist/cjs/Suite.d.ts +139 -0
  33. package/dist/cjs/Suite.d.ts.map +1 -0
  34. package/dist/cjs/Suite.js +221 -0
  35. package/dist/cjs/Suite.js.map +7 -0
  36. package/dist/cjs/Trials.d.ts +93 -0
  37. package/dist/cjs/Trials.d.ts.map +1 -0
  38. package/dist/cjs/Trials.js +82 -0
  39. package/dist/cjs/Trials.js.map +7 -0
  40. package/dist/cjs/index.d.ts +76 -0
  41. package/dist/cjs/index.d.ts.map +1 -0
  42. package/dist/cjs/index.js +51 -0
  43. package/dist/cjs/index.js.map +7 -0
  44. package/dist/cjs/internal/CanonicalJson.d.ts +44 -0
  45. package/dist/cjs/internal/CanonicalJson.d.ts.map +1 -0
  46. package/dist/cjs/internal/CanonicalJson.js +114 -0
  47. package/dist/cjs/internal/CanonicalJson.js.map +7 -0
  48. package/dist/cjs/internal/controlCharacters.d.ts +44 -0
  49. package/dist/cjs/internal/controlCharacters.d.ts.map +1 -0
  50. package/dist/cjs/internal/controlCharacters.js +42 -0
  51. package/dist/cjs/internal/controlCharacters.js.map +7 -0
  52. package/dist/cjs/internal/scorerLabel.d.ts +33 -0
  53. package/dist/cjs/internal/scorerLabel.d.ts.map +1 -0
  54. package/dist/cjs/internal/scorerLabel.js +28 -0
  55. package/dist/cjs/internal/scorerLabel.js.map +7 -0
  56. package/dist/cjs/internal/tupleKey.d.ts +20 -0
  57. package/dist/cjs/internal/tupleKey.d.ts.map +1 -0
  58. package/dist/cjs/internal/tupleKey.js +25 -0
  59. package/dist/cjs/internal/tupleKey.js.map +7 -0
  60. package/dist/cjs/package.json +1 -0
  61. package/dist/esm/Baseline.d.ts +118 -0
  62. package/dist/esm/Baseline.d.ts.map +1 -0
  63. package/dist/esm/Baseline.js +203 -0
  64. package/dist/esm/Baseline.js.map +1 -0
  65. package/dist/esm/CaseExecutor.d.ts +85 -0
  66. package/dist/esm/CaseExecutor.d.ts.map +1 -0
  67. package/dist/esm/CaseExecutor.js +60 -0
  68. package/dist/esm/CaseExecutor.js.map +1 -0
  69. package/dist/esm/EvalError.d.ts +64 -0
  70. package/dist/esm/EvalError.d.ts.map +1 -0
  71. package/dist/esm/EvalError.js +65 -0
  72. package/dist/esm/EvalError.js.map +1 -0
  73. package/dist/esm/Gate.d.ts +55 -0
  74. package/dist/esm/Gate.d.ts.map +1 -0
  75. package/dist/esm/Gate.js +90 -0
  76. package/dist/esm/Gate.js.map +1 -0
  77. package/dist/esm/Regression.d.ts +116 -0
  78. package/dist/esm/Regression.d.ts.map +1 -0
  79. package/dist/esm/Regression.js +195 -0
  80. package/dist/esm/Regression.js.map +1 -0
  81. package/dist/esm/Report.d.ts +114 -0
  82. package/dist/esm/Report.d.ts.map +1 -0
  83. package/dist/esm/Report.js +188 -0
  84. package/dist/esm/Report.js.map +1 -0
  85. package/dist/esm/Runner.d.ts +229 -0
  86. package/dist/esm/Runner.d.ts.map +1 -0
  87. package/dist/esm/Runner.js +430 -0
  88. package/dist/esm/Runner.js.map +1 -0
  89. package/dist/esm/Suite.d.ts +139 -0
  90. package/dist/esm/Suite.d.ts.map +1 -0
  91. package/dist/esm/Suite.js +255 -0
  92. package/dist/esm/Suite.js.map +1 -0
  93. package/dist/esm/Trials.d.ts +93 -0
  94. package/dist/esm/Trials.d.ts.map +1 -0
  95. package/dist/esm/Trials.js +116 -0
  96. package/dist/esm/Trials.js.map +1 -0
  97. package/dist/esm/index.d.ts +76 -0
  98. package/dist/esm/index.d.ts.map +1 -0
  99. package/dist/esm/index.js +76 -0
  100. package/dist/esm/index.js.map +1 -0
  101. package/dist/esm/internal/CanonicalJson.d.ts +44 -0
  102. package/dist/esm/internal/CanonicalJson.d.ts.map +1 -0
  103. package/dist/esm/internal/CanonicalJson.js +202 -0
  104. package/dist/esm/internal/CanonicalJson.js.map +1 -0
  105. package/dist/esm/internal/controlCharacters.d.ts +44 -0
  106. package/dist/esm/internal/controlCharacters.d.ts.map +1 -0
  107. package/dist/esm/internal/controlCharacters.js +65 -0
  108. package/dist/esm/internal/controlCharacters.js.map +1 -0
  109. package/dist/esm/internal/scorerLabel.d.ts +33 -0
  110. package/dist/esm/internal/scorerLabel.d.ts.map +1 -0
  111. package/dist/esm/internal/scorerLabel.js +33 -0
  112. package/dist/esm/internal/scorerLabel.js.map +1 -0
  113. package/dist/esm/internal/tupleKey.d.ts +20 -0
  114. package/dist/esm/internal/tupleKey.d.ts.map +1 -0
  115. package/dist/esm/internal/tupleKey.js +20 -0
  116. package/dist/esm/internal/tupleKey.js.map +1 -0
  117. package/docs/README.md +130 -0
  118. package/docs/api.md +626 -0
  119. package/docs/concepts/determinism.md +64 -0
  120. package/docs/concepts/fixed-suites.md +71 -0
  121. package/docs/concepts/step-keys.md +60 -0
  122. package/docs/guides/author-a-suite.md +115 -0
  123. package/docs/guides/gate-a-run-in-ci.md +129 -0
  124. package/docs/guides/maintain-a-baseline.md +91 -0
  125. package/docs/guides/run-a-suite.md +150 -0
  126. package/docs/installation.md +46 -0
  127. package/docs/quickstart.md +238 -0
  128. package/docs/troubleshooting.md +148 -0
  129. package/package.json +198 -3
  130. package/src/Baseline.ts +259 -0
  131. package/src/CaseExecutor.ts +104 -0
  132. package/src/EvalError.ts +74 -0
  133. package/src/Gate.ts +128 -0
  134. package/src/Regression.ts +314 -0
  135. package/src/Report.ts +268 -0
  136. package/src/Runner.ts +759 -0
  137. package/src/Suite.ts +358 -0
  138. package/src/Trials.ts +154 -0
  139. package/src/index.ts +76 -0
  140. package/src/internal/CanonicalJson.ts +234 -0
  141. package/src/internal/controlCharacters.ts +66 -0
  142. package/src/internal/scorerLabel.ts +35 -0
  143. 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
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }