@smthrs/evals 0.0.0-stage → 1.0.0-rc.13

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 +66 -2
  4. package/dist/cjs/Baseline.d.ts +120 -0
  5. package/dist/cjs/Baseline.d.ts.map +1 -0
  6. package/dist/cjs/Baseline.js +160 -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 +192 -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 +453 -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 +120 -0
  62. package/dist/esm/Baseline.d.ts.map +1 -0
  63. package/dist/esm/Baseline.js +208 -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 +200 -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 +455 -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 +117 -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 +128 -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 +32 -0
  127. package/docs/quickstart.md +238 -0
  128. package/docs/troubleshooting.md +153 -0
  129. package/package.json +198 -3
  130. package/src/Baseline.ts +266 -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 +321 -0
  135. package/src/Report.ts +268 -0
  136. package/src/Runner.ts +789 -0
  137. package/src/Suite.ts +358 -0
  138. package/src/Trials.ts +155 -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,32 @@
1
+ ---
2
+ title: "Installation"
3
+ description: "Install @smthrs/evals from npm, plus its runtime requirements and entry points."
4
+ ---
5
+
6
+ Prereleases are published to npm under the `next` tag. Install the package
7
+ with its `effect` peer dependency:
8
+
9
+ ```bash
10
+ pnpm add @smthrs/evals@next effect@4.0.5
11
+ ```
12
+
13
+ ## Runtime requirements
14
+
15
+ - Node.js 26.4.0 or later, from the package's `engines` field.
16
+ - `effect` `4.0.5`. Suites, runs, baselines, and gates are all `Effect`
17
+ values, so every program composes with the `effect` library directly.
18
+ - `@smthrs/core` supplies `Flow` values. `@smthrs/scorers` supplies scorers,
19
+ bindings, and the pure `@smthrs/scorers/ScoreGate` grading contract, including
20
+ `ScoreGateError`. The evaluation runtime does not load `@smthrs/testing`;
21
+ that package supplies a test facade for development consumers. To run the
22
+ agent behind a case, add [@smthrs/agent](/api/agent) as well.
23
+
24
+ ## Entry points
25
+
26
+ - `@smthrs/evals` exports the nine namespaces: `EvalError`, `Suite`,
27
+ `CaseExecutor`, `Runner`, `Baseline`, `Regression`, `Report`, `Gate`, and
28
+ `Trials`.
29
+ - `@smthrs/evals/<Module>` imports one namespace directly, for example
30
+ `@smthrs/evals/Suite`.
31
+ - `@smthrs/evals/package.json` is exported. The `internal/*` modules and
32
+ 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,153 @@
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
+ - `Target failed for case '<name>': executor returned a stepKey that is not a
95
+ string` or `... a latencyMs that is not a finite non-negative number`: the
96
+ executor succeeded but returned a malformed execution. `path` is
97
+ `cases['<name>'].stepKey` or `cases['<name>'].latencyMs`. Return the step
98
+ key as a string and the latency in milliseconds as a number of 0 or more.
99
+ - `No executor is available for case '<name>'`: the run was provided
100
+ `CaseExecutor.makeNoop()` or `CaseExecutor.layerNoop`. Wire a real executor.
101
+
102
+ ## ambiguous_score_job
103
+
104
+ Two score jobs share a step key and a scorer, so an order-only batch runner
105
+ cannot attribute their results to a case. Give each case its own step key, or
106
+ provide a batch runner that implements `runBatchCorrelated`.
107
+
108
+ ## scorer_protocol
109
+
110
+ A batch runner broke the `runBatch` or `runBatchCorrelated` contract: the
111
+ wrong number of results, a duplicate identity, an unknown identity, or an
112
+ observation that does not echo its job's `targetStepKey` and `scorerKey`.
113
+ Nothing it returned can be trusted. The message names the offending call, as
114
+ in `runBatchCorrelated[0]`. Compare the adapter against `Runner.makeInline()`,
115
+ the reference implementation of the protocol.
116
+
117
+ ## scorer_unavailable
118
+
119
+ No batch runner was available to score with. This is `Runner.layerNoop`
120
+ working as declared: every bound score becomes an inconclusive observation,
121
+ and a gate grades the run undecidable rather than red. When the suite should
122
+ score, provide `Runner.layerInline` or a real adapter.
123
+
124
+ ## Failures that are not EvalError
125
+
126
+ Two wiring mistakes fail outside the `EvalError` channel:
127
+
128
+ - `CaseExecutor.make` with no callback throws a `TypeError` synchronously.
129
+ Pass a function, or an object with a `run` or `execute` callback.
130
+ - `Gate.check` with a threshold outside [0, 1] fails with a `ScoreGateError`,
131
+ code `invalid_threshold`, from [@smthrs/scorers/ScoreGate](/api/scorers). Catch it in
132
+ the error channel and report it.
133
+
134
+ ## Symptoms
135
+
136
+ - Every score is inconclusive. Read the observation's `reason`. A
137
+ `Scorer execution was inconclusive: ...` reason names a scorer or batch
138
+ runner that failed. `<uncoercible cause>` means its failure could not be
139
+ converted to text. Failure reasons use the scorers package's 1024-byte limit.
140
+ A reason about a score outside [0, 1] or an unusable observation kind names a
141
+ scorer that returned a bad result. A suite provided `Runner.layerNoop` also
142
+ lands here.
143
+ - A binding graded nothing. The execution's `target` is not the exact flow
144
+ value the binding's `appliesTo` holds (reference identity, not structure),
145
+ the case failed before scoring, or the sampling policy decided against the
146
+ execution.
147
+ - The gate exits 5. The gate could not decide, or it passed with unresolved
148
+ observations. The Markdown report's Case failures, Missing observations, and
149
+ Inconclusive sections name what is missing. Exit 5 is an unusable harness to
150
+ repair, not a result to read.
151
+ - The report printed a secret. `Report.json` embeds each case's raw
152
+ `execution.output` and redacts nothing. Keep the report out of readable logs
153
+ 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.13",
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.13",
169
+ "@smthrs/scorers": "1.0.0-rc.13"
170
+ },
171
+ "peerDependencies": {
172
+ "effect": "4.0.5"
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.5",
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 examples --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
  }