@smthrs/evals 0.0.0-stage → 1.0.0-rc.3
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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @smthrs/evals
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `Trials`: repeated-run statistics for live evaluations. `passAtK` is the
|
|
8
|
+
unbiased pass@k estimator, `passHatK` the pass^k consistency measure, and
|
|
9
|
+
`summarize` and `aggregate` report both per case and across cases. Bad
|
|
10
|
+
counts throw the new `EvalError` code `invalid_trials`.
|
|
11
|
+
|
|
12
|
+
- `Runner.run` executes under the `Eval.run` span and each case under
|
|
13
|
+
`Eval.case`.
|
|
14
|
+
|
|
15
|
+
- Added `Report.Data`, `Report.data`, and `Report.version`: the serialized
|
|
16
|
+
report shape, the projection that builds it, and the format version stamped
|
|
17
|
+
on it as `reportVersion`. It versions the wire independently of the nested
|
|
18
|
+
`Baseline.version`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Report cells, gate summaries, and runner diagnostics share one scorer label.
|
|
23
|
+
A scorer declared without a name now labels as its key in runner
|
|
24
|
+
diagnostics, the way report cells and gate summaries already labelled it,
|
|
25
|
+
rather than as `scorer (first 8 of the key)`. One unnamed scorer no longer
|
|
26
|
+
appears under two spellings in one report.
|
|
27
|
+
|
|
28
|
+
- **Breaking:** `CaseExecutor.make(run)` takes only the callback. The
|
|
29
|
+
`{ run }` and `{ execute }` object forms and `CaseExecutor.Implementation`
|
|
30
|
+
are gone; pass the function itself. `CaseExecutor.CaseInput` is gone too;
|
|
31
|
+
use `Suite.Case`. 1.0.0-rc.0 retired the `execute` spelling from
|
|
32
|
+
`CaseExecutor.Service` but kept it as a `make` alias, so the package still
|
|
33
|
+
had three spellings for one callback.
|
|
34
|
+
|
|
35
|
+
- **Breaking:** `Report.json` serializes `Report.Data` rather than the
|
|
36
|
+
in-memory `Regression.Report` graph. `run.observations` is now the
|
|
37
|
+
observation table and everything else refers to a row by index:
|
|
38
|
+
`run.cases[i].observations` is a list of indexes and a regression or
|
|
39
|
+
nondeterminism entry's `actual` is a single index. `samples` and
|
|
40
|
+
`inconclusive` are gone from the wire because both are filters of
|
|
41
|
+
`run.observations` by `kind`. The graph carried each observation in up to
|
|
42
|
+
four places, so the artifact was about five times one copy of the
|
|
43
|
+
observations; a run at the declared case ceiling produced 49.2 MiB of JSON
|
|
44
|
+
for 9.9 MiB of observations. `Regression.Report` itself is unchanged, so a
|
|
45
|
+
gate still reads `samples` and `inconclusive` in memory.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- Bound the canonical encoder's total traversal at 2000000 values and 33554432
|
|
50
|
+
output code units, leaving `[budget exceeded]` where a budget runs out. The
|
|
51
|
+
cycle detector tracks ancestors only, so a value referenced twice is expanded
|
|
52
|
+
twice: 24 nested two-child wrappers over one shared leaf are 25 objects at
|
|
53
|
+
depth 24, far below the depth ceiling of 64, and expanded to a multi-megabyte
|
|
54
|
+
artifact in 16 seconds. The budget is spent in traversal order, which the key
|
|
55
|
+
sort fixes, so one value always truncates at the same place.
|
|
56
|
+
|
|
57
|
+
- Load version-1 baselines without a top-level `suite` when every record names
|
|
58
|
+
the same suite. Reject empty or ambiguous legacy artifacts with an explicit
|
|
59
|
+
`invalid_baseline` reason at `suite`.
|
|
60
|
+
- Compute each baseline record's serialization sort key once per write.
|
|
61
|
+
|
|
62
|
+
## [1.0.0-rc.0] - 2026-09-01
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- Added fixed suites, deterministic evaluation runs, baselines, regression
|
|
67
|
+
reports, and score gates.
|
|
68
|
+
- Added `Runner.makeInline` and `Runner.layerInline`, the in-process batch runner
|
|
69
|
+
a run now scores with by default, so a suite with pure scorers no longer has to
|
|
70
|
+
hand-copy an adapter.
|
|
71
|
+
- Added `Suite.limits`, the declared ceilings on concurrency, case count, and
|
|
72
|
+
JSON Lines fixture size.
|
|
73
|
+
- Added `Observation.scorerName` and `BaselineRecord.scorerName`, so a report can
|
|
74
|
+
be read without grepping for a scorer digest.
|
|
75
|
+
- Added `Runner.BatchResult` and the optional `ScoreBatchRunner.runBatchCorrelated`,
|
|
76
|
+
which tags each observation with its job identity. A run correlates by identity
|
|
77
|
+
when a runner implements it, so results may come back in any order.
|
|
78
|
+
- Added `Baseline.suite`, which records artifact ownership even when the baseline
|
|
79
|
+
holds no records. It is required in validated baselines and newly written
|
|
80
|
+
artifacts. `Baseline.load` accepts older version-1 artifacts without it only
|
|
81
|
+
when their nonempty records all name the same suite.
|
|
82
|
+
- Added package-owned documentation in `docs/`, gated by `test/docs.test.ts`.
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
|
|
86
|
+
- **Breaking:** `Runner.run` requires only `CaseExecutor`. It resolved the
|
|
87
|
+
`Runner` service with `Effect.serviceOption` while declaring it in its
|
|
88
|
+
requirements, which made the in-process scoring path unreachable.
|
|
89
|
+
- **Breaking:** `CaseExecutor.Service` is one `run` callback. The twin
|
|
90
|
+
`run`/`execute` pair could diverge, and `make` with neither silently degraded
|
|
91
|
+
to the unavailable executor; it now throws a `TypeError`.
|
|
92
|
+
- **Breaking:** `Baseline.Record` is `Baseline.BaselineRecord`, so the global
|
|
93
|
+
`Record` utility type is usable again.
|
|
94
|
+
- **Breaking:** `Regression.MissingObservation.stepKey` is required, and
|
|
95
|
+
`Regression.Report`'s `samples` and `inconclusive` are narrowed to the
|
|
96
|
+
observation kinds they actually hold.
|
|
97
|
+
- Verify the batch-runner protocol instead of trusting array position. A runner
|
|
98
|
+
that returns the wrong number of observations, or observations identifying
|
|
99
|
+
other jobs, now fails the run with `scorer_protocol` rather than attributing
|
|
100
|
+
one case's score to another. `(targetStepKey, scorerKey)` alone is not a job
|
|
101
|
+
identity: two cases graded by one scorer at one step key still permuted
|
|
102
|
+
silently, so an order-only runner now fails with the new `ambiguous_score_job`
|
|
103
|
+
code before it is called, and a correlated runner is paired by job identity.
|
|
104
|
+
- Decode a batch observation's whole shape rather than spot-checking its score.
|
|
105
|
+
An inconclusive observation with no reason produced an `Observation` that
|
|
106
|
+
violated its own type and made `Report.markdown` throw a `TypeError`.
|
|
107
|
+
- Turn a batch score that is not finite and inside `[0, 1]` into an inconclusive
|
|
108
|
+
observation naming the scorer and the offending value.
|
|
109
|
+
- Fail `Regression.compare` with `invalid_baseline` when the artifact or any of
|
|
110
|
+
its records belongs to another suite. `Baseline.load` derives ownership for
|
|
111
|
+
older suite-less version-1 artifacts only when all records agree on one suite;
|
|
112
|
+
it rejects empty or ambiguous legacy artifacts. Comparisons with the wrong
|
|
113
|
+
suite used to report zero findings.
|
|
114
|
+
- Snapshot and freeze suite cases and bindings, including a binding's sampling
|
|
115
|
+
policy, and reject data that cannot be structured-cloned, so a run stays
|
|
116
|
+
reproducible from the validated suite. `Suite.make` validated at call time and
|
|
117
|
+
copied at execution time, so the two could see different data.
|
|
118
|
+
- Read every option, case, binding, and baseline record field exactly once.
|
|
119
|
+
Reading a field twice let a getter return the value that passed validation and
|
|
120
|
+
then a different one into the suite or the committed artifact.
|
|
121
|
+
- Serialize reports and baselines through one bounded, cycle-safe canonical
|
|
122
|
+
encoder. A cyclic case output used to throw a `RangeError` out of a function
|
|
123
|
+
typed `string`, and a hostile proxy still threw out of the encoder that
|
|
124
|
+
replaced it.
|
|
125
|
+
- Render an `Error` as an object carrying its own enumerable fields, so
|
|
126
|
+
`EvalError.code` and `EvalError.path` survive into `Report.json`. The previous
|
|
127
|
+
`[Name: message]` string dropped the stable branch point a caller reads.
|
|
128
|
+
- Preserve the executor's own code and message on a failed case, and print the
|
|
129
|
+
code in the gate summary, which used to read as a tautology.
|
|
130
|
+
- Name every missing, inconclusive, and failed case in `Report.markdown` instead
|
|
131
|
+
of only counting them, and escape, flatten, and cap every rendered cell.
|
|
132
|
+
- Encode every tuple key injectively instead of joining on `U+0000`, and reject
|
|
133
|
+
control characters in suite and case names.
|
|
134
|
+
- Print the scorer as `name (first 8 of the key)` in gate messages as well as in
|
|
135
|
+
the Markdown report. A missing observation used to name a 64-character digest
|
|
136
|
+
with no way back to the scorer.
|
|
137
|
+
- Split the error vocabulary so a caller can branch on it: added
|
|
138
|
+
`invalid_run_options`, `invalid_tolerance`, `ambiguous_score_job`,
|
|
139
|
+
`scorer_protocol`, and `scorer_unavailable`; added `EvalError.path`; removed
|
|
140
|
+
the unraised `missing_ground_truth`.
|
|
141
|
+
- Raised coverage thresholds to 100% on branches, functions, lines, and
|
|
142
|
+
statements.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Cory and the Smithers Flows contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @smthrs/evals
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Release candidate scope, host requirements and compatibility review are defined in the [library support policy](https://github.com/smithersai/smithers/blob/main/RELEASE_SUPPORT.md).
|
|
4
|
+
|
|
5
|
+
**Documentation:** https://evals.smithers.sh
|
|
6
|
+
|
|
7
|
+
Fixed-suite evaluation for flows: it connects target execution and scorer runners
|
|
8
|
+
to validated suites, committed baselines, regression comparison, reports, and CI
|
|
9
|
+
gates.
|
|
10
|
+
|
|
11
|
+
A unit test asserts equality. A flow that calls a model produces an answer that
|
|
12
|
+
is rarely equal to anything, only better or worse than the last one. This package
|
|
13
|
+
scores the answer, compares the score with a committed baseline, and turns the
|
|
14
|
+
comparison into a CI exit code.
|
|
15
|
+
|
|
16
|
+
Runtime grading belongs to `@smthrs/scorers/ScoreGate`: score samples, verdicts,
|
|
17
|
+
threshold checks, CI grades, and `ScoreGateError` share one pure contract.
|
|
18
|
+
`@smthrs/testing/ScoreGate` is its test facade and also supplies a fixed-suite
|
|
19
|
+
runner. Runtime evaluation code imports scorers directly.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
The package is at 1.0.0-rc.0 and is not on the npm registry yet. It is a
|
|
24
|
+
workspace package of https://github.com/smithersai/smithers, so you use it from
|
|
25
|
+
a package in a clone of that repository. The steps are at
|
|
26
|
+
https://evals.smithers.sh/installation/.
|
|
27
|
+
|
|
28
|
+
## Example
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { CaseExecutor, Runner, Suite } from "@smthrs/evals"
|
|
32
|
+
import { Effect, Layer } from "effect"
|
|
33
|
+
|
|
34
|
+
const greet = ({ name: "greet" })
|
|
35
|
+
|
|
36
|
+
const executor = CaseExecutor.make((suiteCase) =>
|
|
37
|
+
Effect.succeed({
|
|
38
|
+
output: `Hello, ${(suiteCase.input as { readonly name: string }).name}`,
|
|
39
|
+
stepKey: suiteCase.name,
|
|
40
|
+
latencyMs: 0,
|
|
41
|
+
target: greet
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const program = Effect.gen(function*() {
|
|
46
|
+
const suite = yield* Suite.make({
|
|
47
|
+
name: "smoke",
|
|
48
|
+
cases: [{ name: "hello", input: { name: "Ada" } }],
|
|
49
|
+
concurrency: 1
|
|
50
|
+
})
|
|
51
|
+
return yield* Runner.run(suite, { runId: "nightly-2026-01-01", at: "2026-01-01T00:00:00.000Z" })
|
|
52
|
+
}).pipe(Effect.provide(Layer.succeed(CaseExecutor.CaseExecutor)(executor)))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`Runner.run` needs only `CaseExecutor`: scoring runs in process by default.
|
|
56
|
+
Baselines, regression comparison, reports, and gates complete the loop;
|
|
57
|
+
https://evals.smithers.sh/quickstart/ walks all of it end to end.
|
|
58
|
+
|
|
59
|
+
## Reference
|
|
60
|
+
|
|
61
|
+
https://evals.smithers.sh/reference/api/ documents every export, the stable
|
|
62
|
+
failure codes, the batch-runner protocol, the step-key rule that separates a
|
|
63
|
+
regression from nondeterminism, and the declared size and concurrency limits.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical committed evaluation baselines.
|
|
3
|
+
*
|
|
4
|
+
* A baseline is the record of what a suite used to score, committed beside the
|
|
5
|
+
* suite it belongs to. It holds only the fields a comparison reads, and
|
|
6
|
+
* validation rebuilds every record from those fields, so nothing a caller
|
|
7
|
+
* happened to attach to an object travels into a committed artifact.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.1.0
|
|
10
|
+
*/
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import { EvalError } from "./EvalError.ts";
|
|
13
|
+
import type { RunResult } from "./Runner.ts";
|
|
14
|
+
/**
|
|
15
|
+
* Current committed baseline artifact version.
|
|
16
|
+
*
|
|
17
|
+
* @category models
|
|
18
|
+
* @since 0.1.0
|
|
19
|
+
*/
|
|
20
|
+
export declare const version: 1;
|
|
21
|
+
/**
|
|
22
|
+
* One successful score retained by a baseline.
|
|
23
|
+
*
|
|
24
|
+
* `scorer` is the scorer key a comparison matches on. `scorerName` is the
|
|
25
|
+
* optional readable name of the same scorer.
|
|
26
|
+
*
|
|
27
|
+
* @category models
|
|
28
|
+
* @since 0.1.0
|
|
29
|
+
*/
|
|
30
|
+
export interface BaselineRecord {
|
|
31
|
+
readonly suite: string;
|
|
32
|
+
readonly case: string;
|
|
33
|
+
readonly scorer: string;
|
|
34
|
+
readonly scorerName?: string | undefined;
|
|
35
|
+
readonly stepKey: string;
|
|
36
|
+
readonly score: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Canonical committed evaluation baseline.
|
|
40
|
+
*
|
|
41
|
+
* `suite` records artifact ownership even when `records` is empty.
|
|
42
|
+
*
|
|
43
|
+
* @category models
|
|
44
|
+
* @since 0.1.0
|
|
45
|
+
*/
|
|
46
|
+
export interface Baseline {
|
|
47
|
+
readonly version: typeof version;
|
|
48
|
+
readonly suite: string;
|
|
49
|
+
readonly records: ReadonlyArray<BaselineRecord>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The declared ceilings a committed baseline is validated against.
|
|
53
|
+
*
|
|
54
|
+
* `length` bounds the JSON text `load` will parse, so an oversized committed
|
|
55
|
+
* file fails with a sentence instead of exhausting a CI runner's memory.
|
|
56
|
+
*
|
|
57
|
+
* @category models
|
|
58
|
+
* @since 0.1.0
|
|
59
|
+
*/
|
|
60
|
+
export declare const limits: {
|
|
61
|
+
/** Longest baseline JSON text, in UTF-16 code units. */
|
|
62
|
+
readonly length: number;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Builds and validates a baseline from a run's successful observations.
|
|
66
|
+
*
|
|
67
|
+
* Inconclusive observations are dropped: a baseline records what was measured,
|
|
68
|
+
* and an inconclusive observation measured nothing.
|
|
69
|
+
*
|
|
70
|
+
* @category constructors
|
|
71
|
+
* @since 0.1.0
|
|
72
|
+
*/
|
|
73
|
+
export declare const fromRun: (run: RunResult) => Effect.Effect<Baseline, EvalError>;
|
|
74
|
+
/**
|
|
75
|
+
* Validates an in-memory baseline.
|
|
76
|
+
*
|
|
77
|
+
* The result is a snapshot: every known field is read once, then records are
|
|
78
|
+
* rebuilt from those validated values and the array is frozen. Mutating the
|
|
79
|
+
* array or the objects that were passed in cannot change the validated
|
|
80
|
+
* baseline, and a stateful getter cannot substitute a value after validation.
|
|
81
|
+
*
|
|
82
|
+
* Fails with `invalid_baseline` carrying the record index and field name in
|
|
83
|
+
* `path` for a wrong version, a non-array `records`, a record that is not an
|
|
84
|
+
* object, an identity field that is not a string or holds a control
|
|
85
|
+
* character, or a score that is not finite in `[0, 1]`.
|
|
86
|
+
*
|
|
87
|
+
* @category constructors
|
|
88
|
+
* @since 0.1.0
|
|
89
|
+
*/
|
|
90
|
+
export declare const make: (baseline: Omit<Baseline, "version"> & {
|
|
91
|
+
readonly version?: typeof version;
|
|
92
|
+
}) => Effect.Effect<Baseline, EvalError>;
|
|
93
|
+
/**
|
|
94
|
+
* Serializes a baseline with recursively sorted keys and stable numbers.
|
|
95
|
+
*
|
|
96
|
+
* Records are ordered by an injective encoding of `(suite, case, scorer,
|
|
97
|
+
* stepKey)`, so two records whose names differ only in where a separator falls
|
|
98
|
+
* still sort apart. The result ends with a newline and never throws: a
|
|
99
|
+
* validated baseline holds only strings and finite numbers.
|
|
100
|
+
*
|
|
101
|
+
* @category serialization
|
|
102
|
+
* @since 0.1.0
|
|
103
|
+
*/
|
|
104
|
+
export declare const write: (baseline: Baseline) => string;
|
|
105
|
+
/**
|
|
106
|
+
* Loads and validates baseline JSON.
|
|
107
|
+
*
|
|
108
|
+
* A version-1 artifact without a top-level `suite` is accepted only when its
|
|
109
|
+
* nonempty records all name the same suite. That suite becomes the artifact's
|
|
110
|
+
* owner, so writing the result includes it. Empty or ambiguous legacy artifacts
|
|
111
|
+
* fail with `invalid_baseline` at `suite`. Text longer than `limits.length`
|
|
112
|
+
* fails with `invalid_baseline` at `text` before any of it is parsed.
|
|
113
|
+
*
|
|
114
|
+
* @category serialization
|
|
115
|
+
* @since 0.1.0
|
|
116
|
+
*/
|
|
117
|
+
export declare const load: (text: string) => Effect.Effect<Baseline, EvalError>;
|
|
118
|
+
//# sourceMappingURL=Baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Baseline.d.ts","sourceRoot":"","sources":["../../src/Baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAI1C,OAAO,KAAK,EAAe,SAAS,EAAE,MAAM,aAAa,CAAA;AAEzD;;;;;GAKG;AACH,eAAO,MAAM,OAAO,EAAG,CAAU,CAAA;AAEjC;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,OAAO,CAAA;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;CAChD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM;IACjB,wDAAwD;;CAEhD,CAAA;AAiGV;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,KAAK,SAAS,KAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,CAczE,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,IAAI,GACf,UAAU,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,OAAO,CAAA;CAAE,KAC1E,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,CAInC,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAAI,UAAU,QAAQ,KAAG,MAW1C,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,CAMhC,CAAA"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var Baseline_exports = {};
|
|
30
|
+
__export(Baseline_exports, {
|
|
31
|
+
fromRun: () => fromRun,
|
|
32
|
+
limits: () => limits,
|
|
33
|
+
load: () => load,
|
|
34
|
+
make: () => make,
|
|
35
|
+
version: () => version,
|
|
36
|
+
write: () => write
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(Baseline_exports);
|
|
39
|
+
var Effect = __toESM(require("effect/Effect"), 1);
|
|
40
|
+
var import_EvalError = require("./EvalError.js");
|
|
41
|
+
var import_CanonicalJson = require("./internal/CanonicalJson.js");
|
|
42
|
+
var import_controlCharacters = require("./internal/controlCharacters.js");
|
|
43
|
+
var import_tupleKey = require("./internal/tupleKey.js");
|
|
44
|
+
const version = 1;
|
|
45
|
+
const limits = {
|
|
46
|
+
/** Longest baseline JSON text, in UTF-16 code units. */
|
|
47
|
+
length: 16 * 1024 * 1024
|
|
48
|
+
};
|
|
49
|
+
const fail = (message, path) => Effect.fail(
|
|
50
|
+
new import_EvalError.EvalError({ code: "invalid_baseline", message, ...path === void 0 ? {} : { path } })
|
|
51
|
+
);
|
|
52
|
+
const stringField = (value, field, path) => {
|
|
53
|
+
if (typeof value !== "string") {
|
|
54
|
+
return fail(`Baseline record field '${field}' must be a string, got ${typeof value}`, path);
|
|
55
|
+
}
|
|
56
|
+
const control = (0, import_controlCharacters.controlCharacter)(value);
|
|
57
|
+
return control === void 0 ? Effect.succeed(value) : fail(`Baseline record field '${field}' must not contain the control character ${control}`, path);
|
|
58
|
+
};
|
|
59
|
+
const decodeRecord = (value, index) => Effect.gen(function* () {
|
|
60
|
+
const at = `records[${index}]`;
|
|
61
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
62
|
+
return yield* fail(`Baseline record must be an object, got ${value === null ? "null" : typeof value}`, at);
|
|
63
|
+
}
|
|
64
|
+
const source = value;
|
|
65
|
+
const rawSuite = source.suite;
|
|
66
|
+
const rawCase = source.case;
|
|
67
|
+
const rawScorer = source.scorer;
|
|
68
|
+
const rawScorerName = source.scorerName;
|
|
69
|
+
const rawStepKey = source.stepKey;
|
|
70
|
+
const rawScore = source.score;
|
|
71
|
+
const suite = yield* stringField(rawSuite, "suite", `${at}.suite`);
|
|
72
|
+
const caseName = yield* stringField(rawCase, "case", `${at}.case`);
|
|
73
|
+
const scorer = yield* stringField(rawScorer, "scorer", `${at}.scorer`);
|
|
74
|
+
const stepKey = yield* stringField(rawStepKey, "stepKey", `${at}.stepKey`);
|
|
75
|
+
const score = rawScore;
|
|
76
|
+
if (typeof score !== "number" || !Number.isFinite(score) || score < 0 || score > 1) {
|
|
77
|
+
return yield* fail(
|
|
78
|
+
`Baseline record field 'score' must be a finite number in [0, 1], got ${String(score)}`,
|
|
79
|
+
`${at}.score`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const scorerName = rawScorerName === void 0 ? void 0 : yield* stringField(rawScorerName, "scorerName", `${at}.scorerName`);
|
|
83
|
+
return {
|
|
84
|
+
suite,
|
|
85
|
+
case: caseName,
|
|
86
|
+
scorer,
|
|
87
|
+
...scorerName === void 0 ? {} : { scorerName },
|
|
88
|
+
stepKey,
|
|
89
|
+
score: Object.is(score, -0) ? 0 : score
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
const validate = (value) => Effect.gen(function* () {
|
|
93
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
94
|
+
return yield* fail("Baseline must be an object");
|
|
95
|
+
}
|
|
96
|
+
const artifact = value;
|
|
97
|
+
const rawVersion = artifact.version;
|
|
98
|
+
const rawSuite = artifact.suite;
|
|
99
|
+
const rawRecords = artifact.records;
|
|
100
|
+
if (rawVersion !== version) {
|
|
101
|
+
return yield* fail(`Baseline version must be ${version}, got ${String(rawVersion)}`, "version");
|
|
102
|
+
}
|
|
103
|
+
if (rawSuite !== void 0 && typeof rawSuite !== "string") {
|
|
104
|
+
return yield* fail(`Baseline field 'suite' must be a string, got ${typeof rawSuite}`, "suite");
|
|
105
|
+
}
|
|
106
|
+
const suiteControl = rawSuite === void 0 ? void 0 : (0, import_controlCharacters.controlCharacter)(rawSuite);
|
|
107
|
+
if (suiteControl !== void 0) {
|
|
108
|
+
return yield* fail(`Baseline field 'suite' must not contain the control character ${suiteControl}`, "suite");
|
|
109
|
+
}
|
|
110
|
+
if (!Array.isArray(rawRecords)) {
|
|
111
|
+
return yield* fail("Baseline records must be an array", "records");
|
|
112
|
+
}
|
|
113
|
+
const records = yield* Effect.forEach(rawRecords, decodeRecord);
|
|
114
|
+
let suite = rawSuite;
|
|
115
|
+
if (suite === void 0) {
|
|
116
|
+
const first = records[0];
|
|
117
|
+
if (first === void 0) {
|
|
118
|
+
return yield* fail("Cannot infer baseline suite: legacy artifact has no records", "suite");
|
|
119
|
+
}
|
|
120
|
+
if (records.some((record) => record.suite !== first.suite)) {
|
|
121
|
+
return yield* fail("Cannot infer baseline suite: legacy records name multiple suites", "suite");
|
|
122
|
+
}
|
|
123
|
+
suite = first.suite;
|
|
124
|
+
}
|
|
125
|
+
return { version, suite, records: Object.freeze(records) };
|
|
126
|
+
});
|
|
127
|
+
const fromRun = (run) => {
|
|
128
|
+
const records = run.observations.flatMap(
|
|
129
|
+
(observation) => observation.kind === "score" ? [{
|
|
130
|
+
suite: run.suite,
|
|
131
|
+
case: observation.case,
|
|
132
|
+
scorer: observation.scorer,
|
|
133
|
+
...observation.scorerName === void 0 ? {} : { scorerName: observation.scorerName },
|
|
134
|
+
stepKey: observation.stepKey,
|
|
135
|
+
score: observation.score
|
|
136
|
+
}] : []
|
|
137
|
+
);
|
|
138
|
+
return validate({ version, suite: run.suite, records });
|
|
139
|
+
};
|
|
140
|
+
const make = (baseline) => {
|
|
141
|
+
const artifactVersion = baseline.version ?? version;
|
|
142
|
+
const records = baseline.records;
|
|
143
|
+
return validate({ version: artifactVersion, suite: baseline.suite, records });
|
|
144
|
+
};
|
|
145
|
+
const write = (baseline) => {
|
|
146
|
+
const sortKey = (record) => (0, import_tupleKey.tupleKey)(record.suite, record.case, record.scorer, record.stepKey);
|
|
147
|
+
const records = baseline.records.map((record) => [sortKey(record), record]).sort(([left], [right]) => import_CanonicalJson.CanonicalJson.compareText(left, right)).map(([, record]) => record);
|
|
148
|
+
return import_CanonicalJson.CanonicalJson.stringify({
|
|
149
|
+
version: baseline.version,
|
|
150
|
+
suite: baseline.suite,
|
|
151
|
+
records
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const load = (text) => text.length > limits.length ? fail(`Baseline must be at most ${limits.length} characters, got ${text.length}`, "text") : Effect.try({
|
|
155
|
+
try: () => JSON.parse(text),
|
|
156
|
+
catch: (cause) => new import_EvalError.EvalError({ code: "invalid_baseline", message: "Baseline is not valid JSON", cause })
|
|
157
|
+
}).pipe(Effect.flatMap(validate));
|
|
158
|
+
//# sourceMappingURL=Baseline.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/Baseline.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Canonical committed evaluation baselines.\n *\n * A baseline is the record of what a suite used to score, committed beside the\n * suite it belongs to. It holds only the fields a comparison reads, and\n * validation rebuilds every record from those fields, so nothing a caller\n * happened to attach to an object travels into a committed artifact.\n *\n * @since 0.1.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport { EvalError } from \"./EvalError.ts\"\nimport { CanonicalJson } from \"./internal/CanonicalJson.ts\"\nimport { controlCharacter } from \"./internal/controlCharacters.ts\"\nimport { tupleKey } from \"./internal/tupleKey.ts\"\nimport type { Observation, RunResult } from \"./Runner.ts\"\n\n/**\n * Current committed baseline artifact version.\n *\n * @category models\n * @since 0.1.0\n */\nexport const version = 1 as const\n\n/**\n * One successful score retained by a baseline.\n *\n * `scorer` is the scorer key a comparison matches on. `scorerName` is the\n * optional readable name of the same scorer.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface BaselineRecord {\n readonly suite: string\n readonly case: string\n readonly scorer: string\n readonly scorerName?: string | undefined\n readonly stepKey: string\n readonly score: number\n}\n\n/**\n * Canonical committed evaluation baseline.\n *\n * `suite` records artifact ownership even when `records` is empty.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Baseline {\n readonly version: typeof version\n readonly suite: string\n readonly records: ReadonlyArray<BaselineRecord>\n}\n\n/**\n * The declared ceilings a committed baseline is validated against.\n *\n * `length` bounds the JSON text `load` will parse, so an oversized committed\n * file fails with a sentence instead of exhausting a CI runner's memory.\n *\n * @category models\n * @since 0.1.0\n */\nexport const limits = {\n /** Longest baseline JSON text, in UTF-16 code units. */\n length: 16 * 1024 * 1024\n} as const\n\nconst fail = (message: string, path?: string): Effect.Effect<never, EvalError> =>\n Effect.fail(\n new EvalError({ code: \"invalid_baseline\", message, ...(path === undefined ? {} : { path }) })\n )\n\n// A committed record is read by a gate whose summary goes to a CI log, so it\n// follows the same control-character rule as a suite name: rejected where it\n// enters the system, not flattened later.\nconst stringField = (value: unknown, field: string, path: string): Effect.Effect<string, EvalError> => {\n if (typeof value !== \"string\") {\n return fail(`Baseline record field '${field}' must be a string, got ${typeof value}`, path)\n }\n const control = controlCharacter(value)\n return control === undefined\n ? Effect.succeed(value)\n : fail(`Baseline record field '${field}' must not contain the control character ${control}`, path)\n}\n\nconst decodeRecord = (value: unknown, index: number): Effect.Effect<BaselineRecord, EvalError> =>\n Effect.gen(function*() {\n const at = `records[${index}]`\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return yield* fail(`Baseline record must be an object, got ${value === null ? \"null\" : typeof value}`, at)\n }\n const source = value as { readonly [key: string]: unknown }\n const rawSuite = source.suite\n const rawCase = source.case\n const rawScorer = source.scorer\n const rawScorerName = source.scorerName\n const rawStepKey = source.stepKey\n const rawScore = source.score\n const suite = yield* stringField(rawSuite, \"suite\", `${at}.suite`)\n const caseName = yield* stringField(rawCase, \"case\", `${at}.case`)\n const scorer = yield* stringField(rawScorer, \"scorer\", `${at}.scorer`)\n const stepKey = yield* stringField(rawStepKey, \"stepKey\", `${at}.stepKey`)\n const score = rawScore\n if (typeof score !== \"number\" || !Number.isFinite(score) || score < 0 || score > 1) {\n return yield* fail(\n `Baseline record field 'score' must be a finite number in [0, 1], got ${String(score)}`,\n `${at}.score`\n )\n }\n const scorerName = rawScorerName === undefined\n ? undefined\n : yield* stringField(rawScorerName, \"scorerName\", `${at}.scorerName`)\n // Every record is rebuilt field by field. Keeping the caller's object would\n // carry unknown keys, and any getter among them, straight into a committed\n // artifact.\n return {\n suite,\n case: caseName,\n scorer,\n ...(scorerName === undefined ? {} : { scorerName }),\n stepKey,\n score: Object.is(score, -0) ? 0 : score\n }\n })\n\nconst validate = (value: unknown): Effect.Effect<Baseline, EvalError> =>\n Effect.gen(function*() {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return yield* fail(\"Baseline must be an object\")\n }\n const artifact = value as { readonly version?: unknown; readonly suite?: unknown; readonly records?: unknown }\n const rawVersion = artifact.version\n const rawSuite = artifact.suite\n const rawRecords = artifact.records\n if (rawVersion !== version) {\n return yield* fail(`Baseline version must be ${version}, got ${String(rawVersion)}`, \"version\")\n }\n if (rawSuite !== undefined && typeof rawSuite !== \"string\") {\n return yield* fail(`Baseline field 'suite' must be a string, got ${typeof rawSuite}`, \"suite\")\n }\n const suiteControl = rawSuite === undefined ? undefined : controlCharacter(rawSuite)\n if (suiteControl !== undefined) {\n return yield* fail(`Baseline field 'suite' must not contain the control character ${suiteControl}`, \"suite\")\n }\n if (!Array.isArray(rawRecords)) {\n return yield* fail(\"Baseline records must be an array\", \"records\")\n }\n const records = yield* Effect.forEach(rawRecords, decodeRecord)\n let suite = rawSuite\n if (suite === undefined) {\n const first = records[0]\n if (first === undefined) {\n return yield* fail(\"Cannot infer baseline suite: legacy artifact has no records\", \"suite\")\n }\n if (records.some((record) => record.suite !== first.suite)) {\n return yield* fail(\"Cannot infer baseline suite: legacy records name multiple suites\", \"suite\")\n }\n suite = first.suite\n }\n return { version, suite, records: Object.freeze(records) }\n })\n\n/**\n * Builds and validates a baseline from a run's successful observations.\n *\n * Inconclusive observations are dropped: a baseline records what was measured,\n * and an inconclusive observation measured nothing.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const fromRun = (run: RunResult): Effect.Effect<Baseline, EvalError> => {\n const records: Array<BaselineRecord> = run.observations.flatMap((observation: Observation) =>\n observation.kind === \"score\"\n ? [{\n suite: run.suite,\n case: observation.case,\n scorer: observation.scorer,\n ...(observation.scorerName === undefined ? {} : { scorerName: observation.scorerName }),\n stepKey: observation.stepKey,\n score: observation.score\n }]\n : []\n )\n return validate({ version, suite: run.suite, records })\n}\n\n/**\n * Validates an in-memory baseline.\n *\n * The result is a snapshot: every known field is read once, then records are\n * rebuilt from those validated values and the array is frozen. Mutating the\n * array or the objects that were passed in cannot change the validated\n * baseline, and a stateful getter cannot substitute a value after validation.\n *\n * Fails with `invalid_baseline` carrying the record index and field name in\n * `path` for a wrong version, a non-array `records`, a record that is not an\n * object, an identity field that is not a string or holds a control\n * character, or a score that is not finite in `[0, 1]`.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (\n baseline: Omit<Baseline, \"version\"> & { readonly version?: typeof version }\n): Effect.Effect<Baseline, EvalError> => {\n const artifactVersion = baseline.version ?? version\n const records = baseline.records\n return validate({ version: artifactVersion, suite: baseline.suite, records })\n}\n\n/**\n * Serializes a baseline with recursively sorted keys and stable numbers.\n *\n * Records are ordered by an injective encoding of `(suite, case, scorer,\n * stepKey)`, so two records whose names differ only in where a separator falls\n * still sort apart. The result ends with a newline and never throws: a\n * validated baseline holds only strings and finite numbers.\n *\n * @category serialization\n * @since 0.1.0\n */\nexport const write = (baseline: Baseline): string => {\n const sortKey = (record: BaselineRecord): string => tupleKey(record.suite, record.case, record.scorer, record.stepKey)\n const records = baseline.records\n .map((record) => [sortKey(record), record] as const)\n .sort(([left], [right]) => CanonicalJson.compareText(left, right))\n .map(([, record]) => record)\n return CanonicalJson.stringify({\n version: baseline.version,\n suite: baseline.suite,\n records\n })\n}\n\n/**\n * Loads and validates baseline JSON.\n *\n * A version-1 artifact without a top-level `suite` is accepted only when its\n * nonempty records all name the same suite. That suite becomes the artifact's\n * owner, so writing the result includes it. Empty or ambiguous legacy artifacts\n * fail with `invalid_baseline` at `suite`. Text longer than `limits.length`\n * fails with `invalid_baseline` at `text` before any of it is parsed.\n *\n * @category serialization\n * @since 0.1.0\n */\nexport const load = (text: string): Effect.Effect<Baseline, EvalError> =>\n text.length > limits.length\n ? fail(`Baseline must be at most ${limits.length} characters, got ${text.length}`, \"text\")\n : Effect.try({\n try: () => JSON.parse(text) as unknown,\n catch: (cause) => new EvalError({ code: \"invalid_baseline\", message: \"Baseline is not valid JSON\", cause })\n }).pipe(Effect.flatMap(validate))\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injectable target-flow execution boundary.
|
|
3
|
+
*
|
|
4
|
+
* A suite says what to run; this service says how. Keeping it injectable is
|
|
5
|
+
* what lets the same suite run against a real flow, a scripted host, or nothing
|
|
6
|
+
* at all, and it is the only place a target's failure is converted into a typed
|
|
7
|
+
* evaluation failure.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.1.0
|
|
10
|
+
*/
|
|
11
|
+
import type { Flow } from "@smthrs/core";
|
|
12
|
+
import * as Context from "effect/Context";
|
|
13
|
+
import * as Effect from "effect/Effect";
|
|
14
|
+
import * as Layer from "effect/Layer";
|
|
15
|
+
import { EvalError } from "./EvalError.ts";
|
|
16
|
+
import type { Case } from "./Suite.ts";
|
|
17
|
+
/**
|
|
18
|
+
* The result of executing one target-flow case.
|
|
19
|
+
*
|
|
20
|
+
* `target` is the flow value the case actually executed. A run matches it
|
|
21
|
+
* against each binding's `appliesTo` by reference identity, so it has to be the
|
|
22
|
+
* declared flow itself rather than a copy of it.
|
|
23
|
+
*
|
|
24
|
+
* @category models
|
|
25
|
+
* @since 0.1.0
|
|
26
|
+
*/
|
|
27
|
+
export interface Execution {
|
|
28
|
+
readonly output: unknown;
|
|
29
|
+
readonly stepKey: string;
|
|
30
|
+
readonly latencyMs: number;
|
|
31
|
+
readonly target: Pick<Flow.Any, "name">;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The one callback a case executor is.
|
|
35
|
+
*
|
|
36
|
+
* @category models
|
|
37
|
+
* @since 0.1.0
|
|
38
|
+
*/
|
|
39
|
+
export type Run = (suiteCase: Case) => Effect.Effect<Execution, EvalError>;
|
|
40
|
+
/**
|
|
41
|
+
* Runtime shape for an injectable target-flow executor.
|
|
42
|
+
*
|
|
43
|
+
* @category services
|
|
44
|
+
* @since 0.1.0
|
|
45
|
+
*/
|
|
46
|
+
export interface Service {
|
|
47
|
+
readonly run: Run;
|
|
48
|
+
}
|
|
49
|
+
declare const CaseExecutor_base: Context.ServiceClass<CaseExecutor, "flows/evals/CaseExecutor", Service>;
|
|
50
|
+
/**
|
|
51
|
+
* Injectable execution boundary for a target flow.
|
|
52
|
+
*
|
|
53
|
+
* @category services
|
|
54
|
+
* @since 0.1.0
|
|
55
|
+
*/
|
|
56
|
+
export declare class CaseExecutor extends CaseExecutor_base {
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Builds an executor from its one callback.
|
|
60
|
+
*
|
|
61
|
+
* Throws a `TypeError` when there is no callback. An executor that silently
|
|
62
|
+
* degraded to {@link makeNoop} turned one wiring mistake into a whole suite of
|
|
63
|
+
* cases failing with `executor`, which reads as a broken target rather than a
|
|
64
|
+
* missing one.
|
|
65
|
+
*
|
|
66
|
+
* @category constructors
|
|
67
|
+
* @since 0.1.0
|
|
68
|
+
*/
|
|
69
|
+
export declare const make: (run: Run) => Service;
|
|
70
|
+
/**
|
|
71
|
+
* Builds an executor that fails every case with a typed executor error.
|
|
72
|
+
*
|
|
73
|
+
* @category constructors
|
|
74
|
+
* @since 0.1.0
|
|
75
|
+
*/
|
|
76
|
+
export declare const makeNoop: () => Service;
|
|
77
|
+
/**
|
|
78
|
+
* Provides the unavailable executor.
|
|
79
|
+
*
|
|
80
|
+
* @category layers
|
|
81
|
+
* @since 0.1.0
|
|
82
|
+
*/
|
|
83
|
+
export declare const layerNoop: Layer.Layer<CaseExecutor>;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=CaseExecutor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CaseExecutor.d.ts","sourceRoot":"","sources":["../../src/CaseExecutor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC;;;;;;;;;GASG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;CACxC;AAED;;;;;GAKG;AACH,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,EAAE,IAAI,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAE1E;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;CAClB;;AAED;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,iBAAoE;CAAG;AAEzG;;;;;;;;;;GAUG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,GAAG,KAAG,OAK/B,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,QAAO,OAUxB,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,YAAY,CAA2C,CAAA"}
|