@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
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
- # Temporary Holding Version
1
+ # @smthrs/evals
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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"}