@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +62 -2
  4. package/dist/cjs/Baseline.d.ts +118 -0
  5. package/dist/cjs/Baseline.d.ts.map +1 -0
  6. package/dist/cjs/Baseline.js +158 -0
  7. package/dist/cjs/Baseline.js.map +7 -0
  8. package/dist/cjs/CaseExecutor.d.ts +85 -0
  9. package/dist/cjs/CaseExecutor.d.ts.map +1 -0
  10. package/dist/cjs/CaseExecutor.js +59 -0
  11. package/dist/cjs/CaseExecutor.js.map +7 -0
  12. package/dist/cjs/EvalError.d.ts +64 -0
  13. package/dist/cjs/EvalError.d.ts.map +1 -0
  14. package/dist/cjs/EvalError.js +54 -0
  15. package/dist/cjs/EvalError.js.map +7 -0
  16. package/dist/cjs/Gate.d.ts +55 -0
  17. package/dist/cjs/Gate.d.ts.map +1 -0
  18. package/dist/cjs/Gate.js +88 -0
  19. package/dist/cjs/Gate.js.map +7 -0
  20. package/dist/cjs/Regression.d.ts +116 -0
  21. package/dist/cjs/Regression.d.ts.map +1 -0
  22. package/dist/cjs/Regression.js +191 -0
  23. package/dist/cjs/Regression.js.map +7 -0
  24. package/dist/cjs/Report.d.ts +114 -0
  25. package/dist/cjs/Report.d.ts.map +1 -0
  26. package/dist/cjs/Report.js +161 -0
  27. package/dist/cjs/Report.js.map +7 -0
  28. package/dist/cjs/Runner.d.ts +229 -0
  29. package/dist/cjs/Runner.d.ts.map +1 -0
  30. package/dist/cjs/Runner.js +430 -0
  31. package/dist/cjs/Runner.js.map +7 -0
  32. package/dist/cjs/Suite.d.ts +139 -0
  33. package/dist/cjs/Suite.d.ts.map +1 -0
  34. package/dist/cjs/Suite.js +221 -0
  35. package/dist/cjs/Suite.js.map +7 -0
  36. package/dist/cjs/Trials.d.ts +93 -0
  37. package/dist/cjs/Trials.d.ts.map +1 -0
  38. package/dist/cjs/Trials.js +82 -0
  39. package/dist/cjs/Trials.js.map +7 -0
  40. package/dist/cjs/index.d.ts +76 -0
  41. package/dist/cjs/index.d.ts.map +1 -0
  42. package/dist/cjs/index.js +51 -0
  43. package/dist/cjs/index.js.map +7 -0
  44. package/dist/cjs/internal/CanonicalJson.d.ts +44 -0
  45. package/dist/cjs/internal/CanonicalJson.d.ts.map +1 -0
  46. package/dist/cjs/internal/CanonicalJson.js +114 -0
  47. package/dist/cjs/internal/CanonicalJson.js.map +7 -0
  48. package/dist/cjs/internal/controlCharacters.d.ts +44 -0
  49. package/dist/cjs/internal/controlCharacters.d.ts.map +1 -0
  50. package/dist/cjs/internal/controlCharacters.js +42 -0
  51. package/dist/cjs/internal/controlCharacters.js.map +7 -0
  52. package/dist/cjs/internal/scorerLabel.d.ts +33 -0
  53. package/dist/cjs/internal/scorerLabel.d.ts.map +1 -0
  54. package/dist/cjs/internal/scorerLabel.js +28 -0
  55. package/dist/cjs/internal/scorerLabel.js.map +7 -0
  56. package/dist/cjs/internal/tupleKey.d.ts +20 -0
  57. package/dist/cjs/internal/tupleKey.d.ts.map +1 -0
  58. package/dist/cjs/internal/tupleKey.js +25 -0
  59. package/dist/cjs/internal/tupleKey.js.map +7 -0
  60. package/dist/cjs/package.json +1 -0
  61. package/dist/esm/Baseline.d.ts +118 -0
  62. package/dist/esm/Baseline.d.ts.map +1 -0
  63. package/dist/esm/Baseline.js +203 -0
  64. package/dist/esm/Baseline.js.map +1 -0
  65. package/dist/esm/CaseExecutor.d.ts +85 -0
  66. package/dist/esm/CaseExecutor.d.ts.map +1 -0
  67. package/dist/esm/CaseExecutor.js +60 -0
  68. package/dist/esm/CaseExecutor.js.map +1 -0
  69. package/dist/esm/EvalError.d.ts +64 -0
  70. package/dist/esm/EvalError.d.ts.map +1 -0
  71. package/dist/esm/EvalError.js +65 -0
  72. package/dist/esm/EvalError.js.map +1 -0
  73. package/dist/esm/Gate.d.ts +55 -0
  74. package/dist/esm/Gate.d.ts.map +1 -0
  75. package/dist/esm/Gate.js +90 -0
  76. package/dist/esm/Gate.js.map +1 -0
  77. package/dist/esm/Regression.d.ts +116 -0
  78. package/dist/esm/Regression.d.ts.map +1 -0
  79. package/dist/esm/Regression.js +195 -0
  80. package/dist/esm/Regression.js.map +1 -0
  81. package/dist/esm/Report.d.ts +114 -0
  82. package/dist/esm/Report.d.ts.map +1 -0
  83. package/dist/esm/Report.js +188 -0
  84. package/dist/esm/Report.js.map +1 -0
  85. package/dist/esm/Runner.d.ts +229 -0
  86. package/dist/esm/Runner.d.ts.map +1 -0
  87. package/dist/esm/Runner.js +430 -0
  88. package/dist/esm/Runner.js.map +1 -0
  89. package/dist/esm/Suite.d.ts +139 -0
  90. package/dist/esm/Suite.d.ts.map +1 -0
  91. package/dist/esm/Suite.js +255 -0
  92. package/dist/esm/Suite.js.map +1 -0
  93. package/dist/esm/Trials.d.ts +93 -0
  94. package/dist/esm/Trials.d.ts.map +1 -0
  95. package/dist/esm/Trials.js +116 -0
  96. package/dist/esm/Trials.js.map +1 -0
  97. package/dist/esm/index.d.ts +76 -0
  98. package/dist/esm/index.d.ts.map +1 -0
  99. package/dist/esm/index.js +76 -0
  100. package/dist/esm/index.js.map +1 -0
  101. package/dist/esm/internal/CanonicalJson.d.ts +44 -0
  102. package/dist/esm/internal/CanonicalJson.d.ts.map +1 -0
  103. package/dist/esm/internal/CanonicalJson.js +202 -0
  104. package/dist/esm/internal/CanonicalJson.js.map +1 -0
  105. package/dist/esm/internal/controlCharacters.d.ts +44 -0
  106. package/dist/esm/internal/controlCharacters.d.ts.map +1 -0
  107. package/dist/esm/internal/controlCharacters.js +65 -0
  108. package/dist/esm/internal/controlCharacters.js.map +1 -0
  109. package/dist/esm/internal/scorerLabel.d.ts +33 -0
  110. package/dist/esm/internal/scorerLabel.d.ts.map +1 -0
  111. package/dist/esm/internal/scorerLabel.js +33 -0
  112. package/dist/esm/internal/scorerLabel.js.map +1 -0
  113. package/dist/esm/internal/tupleKey.d.ts +20 -0
  114. package/dist/esm/internal/tupleKey.d.ts.map +1 -0
  115. package/dist/esm/internal/tupleKey.js +20 -0
  116. package/dist/esm/internal/tupleKey.js.map +1 -0
  117. package/docs/README.md +130 -0
  118. package/docs/api.md +626 -0
  119. package/docs/concepts/determinism.md +64 -0
  120. package/docs/concepts/fixed-suites.md +71 -0
  121. package/docs/concepts/step-keys.md +60 -0
  122. package/docs/guides/author-a-suite.md +115 -0
  123. package/docs/guides/gate-a-run-in-ci.md +129 -0
  124. package/docs/guides/maintain-a-baseline.md +91 -0
  125. package/docs/guides/run-a-suite.md +150 -0
  126. package/docs/installation.md +46 -0
  127. package/docs/quickstart.md +238 -0
  128. package/docs/troubleshooting.md +148 -0
  129. package/package.json +198 -3
  130. package/src/Baseline.ts +259 -0
  131. package/src/CaseExecutor.ts +104 -0
  132. package/src/EvalError.ts +74 -0
  133. package/src/Gate.ts +128 -0
  134. package/src/Regression.ts +314 -0
  135. package/src/Report.ts +268 -0
  136. package/src/Runner.ts +759 -0
  137. package/src/Suite.ts +358 -0
  138. package/src/Trials.ts +154 -0
  139. package/src/index.ts +76 -0
  140. package/src/internal/CanonicalJson.ts +234 -0
  141. package/src/internal/controlCharacters.ts +66 -0
  142. package/src/internal/scorerLabel.ts +35 -0
  143. package/src/internal/tupleKey.ts +20 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Deterministic fixed-suite execution and bound scorer evaluation.
3
+ *
4
+ * A run executes every case through the injected {@link CaseExecutor}, then
5
+ * grades each execution with the scorers bound to the flow it executed. The
6
+ * timestamp and the run identity come from the caller, so two runs of the same
7
+ * suite over the same inputs produce byte-identical observations.
8
+ *
9
+ * @since 0.1.0
10
+ */
11
+ import * as ScorerRunner from "@smthrs/scorers/Runner";
12
+ import type * as ScoreStore from "@smthrs/scorers/ScoreStore";
13
+ import * as Context from "effect/Context";
14
+ import * as Effect from "effect/Effect";
15
+ import * as Layer from "effect/Layer";
16
+ import { CaseExecutor, type Execution } from "./CaseExecutor.ts";
17
+ import { EvalError } from "./EvalError.ts";
18
+ import type { Binding, Suite } from "./Suite.ts";
19
+ import * as Trials from "./Trials.ts";
20
+ /**
21
+ * One score observation emitted by a suite run.
22
+ *
23
+ * `scorer` is the scorer key, a digest of the scorer's own declaration, which
24
+ * is what a baseline matches on. `scorerName` is the human name from the same
25
+ * declaration, carried alongside so a report can be read without grepping for
26
+ * the digest. `at` is the run's timestamp, not the scorer's: a run's
27
+ * observations all carry one instant so a baseline stays reproducible.
28
+ *
29
+ * @category models
30
+ * @since 0.1.0
31
+ */
32
+ export type Observation = {
33
+ readonly case: string;
34
+ readonly scorer: string;
35
+ readonly scorerName?: string | undefined;
36
+ readonly stepKey: string;
37
+ readonly at: string;
38
+ } & ({
39
+ readonly kind: "score";
40
+ readonly score: number;
41
+ readonly reason?: string | undefined;
42
+ readonly meta?: unknown;
43
+ } | {
44
+ readonly kind: "inconclusive";
45
+ readonly reason: string;
46
+ readonly meta?: unknown;
47
+ });
48
+ /**
49
+ * A request sent to the scorers batch runner.
50
+ *
51
+ * @category models
52
+ * @since 0.1.0
53
+ */
54
+ export interface ScoreRequest {
55
+ readonly case: string;
56
+ readonly stepKey: string;
57
+ readonly binding: Binding;
58
+ readonly input: {
59
+ readonly input: unknown;
60
+ readonly output: unknown;
61
+ readonly groundTruth?: unknown;
62
+ readonly context?: unknown;
63
+ readonly latencyMs?: number;
64
+ };
65
+ }
66
+ /**
67
+ * A blocking scorer job, matching the `Runner` module of `@smthrs/scorers`.
68
+ *
69
+ * @category models
70
+ * @since 0.1.0
71
+ */
72
+ export type ScoreJob = ScorerRunner.Job;
73
+ /**
74
+ * Structural adapter for `@smthrs/scorers`' blocking batch runner.
75
+ *
76
+ * A runner that implements `runBatchCorrelated` tags every observation with
77
+ * its job identity and may return results in any order. A runner that implements
78
+ * only `runBatch` is correlated positionally: it must return exactly one
79
+ * observation per job, in order, and each observation must echo its job's
80
+ * `targetStepKey` and `scorerKey`. A run refuses the order-only protocol before
81
+ * scoring when two jobs share that pair, because their results could not be
82
+ * attributed to a case.
83
+ *
84
+ * A run fails with `scorer_protocol` when either protocol is broken.
85
+ * `@smthrs/scorers`' `Runner.Service` implements `runBatchCorrelated`, so its
86
+ * service value can be used directly and `ambiguous_score_job` never applies
87
+ * to it.
88
+ *
89
+ * @category services
90
+ * @since 0.1.0
91
+ */
92
+ export interface ScoreBatchRunner {
93
+ readonly runBatch: (jobs: ReadonlyArray<ScoreJob>, options?: {
94
+ readonly concurrency?: number | undefined;
95
+ }) => Effect.Effect<ReadonlyArray<ScoreObservation>, unknown>;
96
+ readonly runBatchCorrelated?: ((jobs: ReadonlyArray<ScoreJob>, options?: {
97
+ readonly concurrency?: number | undefined;
98
+ }) => Effect.Effect<ReadonlyArray<BatchResult>, unknown>) | undefined;
99
+ }
100
+ /**
101
+ * A score result aligned with a {@link ScoreRequest}.
102
+ *
103
+ * @category models
104
+ * @since 0.1.0
105
+ */
106
+ export type ScoreObservation = ScoreStore.Observation;
107
+ /**
108
+ * A batch result tagged with the identity of the job that produced it.
109
+ *
110
+ * @category models
111
+ * @since 0.1.0
112
+ */
113
+ export interface BatchResult {
114
+ readonly identity: string;
115
+ readonly observation: ScoreObservation;
116
+ }
117
+ /**
118
+ * Per-case result retained by the deterministic runner.
119
+ *
120
+ * @category models
121
+ * @since 0.1.0
122
+ */
123
+ export interface CaseResult {
124
+ readonly case: string;
125
+ readonly execution?: Execution | undefined;
126
+ readonly error?: EvalError | undefined;
127
+ readonly observations: ReadonlyArray<Observation>;
128
+ readonly trials?: Trials.Summary | undefined;
129
+ }
130
+ /**
131
+ * Stable result of a suite run.
132
+ *
133
+ * @category models
134
+ * @since 0.1.0
135
+ */
136
+ export interface RunResult {
137
+ readonly runId: string;
138
+ readonly suite: string;
139
+ readonly cases: ReadonlyArray<CaseResult>;
140
+ readonly observations: ReadonlyArray<Observation>;
141
+ readonly trials?: Trials.Aggregate | undefined;
142
+ readonly k?: number | undefined;
143
+ }
144
+ /**
145
+ * Options for a deterministic suite run.
146
+ *
147
+ * @category models
148
+ * @since 0.1.0
149
+ */
150
+ export interface RunOptions {
151
+ readonly scorer?: ScoreBatchRunner | undefined;
152
+ readonly runId: string;
153
+ readonly sampleId?: string | undefined;
154
+ readonly trials?: number | undefined;
155
+ readonly k?: number | undefined;
156
+ readonly at: string;
157
+ }
158
+ declare const Runner_base: Context.ServiceClass<Runner, "flows/evals/Runner", ScoreBatchRunner>;
159
+ /**
160
+ * Injectable batch-runner service used when a caller wants a reusable adapter.
161
+ *
162
+ * {@link run} does not require it: a run scores with `options.scorer` when one
163
+ * is passed, otherwise with this service when one is provided, otherwise
164
+ * in process through {@link makeInline}.
165
+ *
166
+ * @category services
167
+ * @since 0.1.0
168
+ */
169
+ export declare class Runner extends Runner_base {
170
+ }
171
+ /**
172
+ * Builds the in-process batch runner a run scores with by default.
173
+ *
174
+ * Each job's scorer runs in the current process, its result is checked by
175
+ * `@smthrs/scorers`' own `Scorer.validate`, and a scorer that fails becomes an
176
+ * inconclusive observation naming its cause rather than failing the run. It
177
+ * honours the {@link ScoreBatchRunner} protocol, so it is also the reference
178
+ * a custom adapter can be compared against.
179
+ *
180
+ * @category constructors
181
+ * @since 0.1.0
182
+ */
183
+ export declare const makeInline: () => ScoreBatchRunner;
184
+ /**
185
+ * Provides the in-process batch runner built by {@link makeInline}.
186
+ *
187
+ * @category layers
188
+ * @since 0.1.0
189
+ */
190
+ export declare const layerInline: Layer.Layer<Runner>;
191
+ /**
192
+ * Runs a fixed suite with bounded execution and declaration-order results.
193
+ *
194
+ * Cases run through the provided {@link CaseExecutor} at the suite's
195
+ * concurrency and are returned in declaration order. Each executor receives
196
+ * its own mutable case copy. Each scorer receives independent copies of the
197
+ * original case input, expected value, and binding data. Every execution is then
198
+ * graded by the bindings whose `appliesTo` is the flow the execution reports as
199
+ * its target, matched by reference identity, so a binding attached to another
200
+ * flow contributes no observations.
201
+ *
202
+ * Scoring goes through `options.scorer` when the caller passes one, the
203
+ * {@link Runner} service when one is provided, and {@link makeInline}
204
+ * otherwise. A case whose target failed keeps its typed error, locates itself
205
+ * with the executor's own `path` or with `cases['<name>']`, and produces no
206
+ * observations; a scorer that failed produces an inconclusive observation. The
207
+ * run itself fails only with `invalid_run_options` for a non-canonical run
208
+ * identity or timestamp, `invalid_suite` for an unusable sampling policy,
209
+ * `ambiguous_score_job` when an order-only runner cannot distinguish two jobs,
210
+ * or `scorer_protocol` for a batch runner that broke the
211
+ * {@link ScoreBatchRunner} contract.
212
+ *
213
+ * @category constructors
214
+ * @since 0.1.0
215
+ */
216
+ export declare const run: (suite: Suite, options: RunOptions) => Effect.Effect<RunResult, EvalError, CaseExecutor>;
217
+ /**
218
+ * Provides a batch runner that is never available.
219
+ *
220
+ * Every bound score under this layer becomes an inconclusive observation, which
221
+ * a gate grades as an undecidable run rather than a red. Provide it to state
222
+ * that a suite must not score, and {@link layerInline} to score in process.
223
+ *
224
+ * @category layers
225
+ * @since 0.1.0
226
+ */
227
+ export declare const layerNoop: Layer.Layer<Runner>;
228
+ export {};
229
+ //# sourceMappingURL=Runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Runner.d.ts","sourceRoot":"","sources":["../../src/Runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,430 @@
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 Runner_exports = {};
30
+ __export(Runner_exports, {
31
+ Runner: () => Runner,
32
+ layerInline: () => layerInline,
33
+ layerNoop: () => layerNoop,
34
+ makeInline: () => makeInline,
35
+ run: () => run
36
+ });
37
+ module.exports = __toCommonJS(Runner_exports);
38
+ var ScorerRunner = __toESM(require("@smthrs/scorers/Runner"), 1);
39
+ var Sampling = __toESM(require("@smthrs/scorers/Sampling"), 1);
40
+ var Scorer = __toESM(require("@smthrs/scorers/Scorer"), 1);
41
+ var Cause = __toESM(require("effect/Cause"), 1);
42
+ var Context = __toESM(require("effect/Context"), 1);
43
+ var Effect = __toESM(require("effect/Effect"), 1);
44
+ var Layer = __toESM(require("effect/Layer"), 1);
45
+ var import_CaseExecutor = require("./CaseExecutor.js");
46
+ var import_EvalError = require("./EvalError.js");
47
+ var import_controlCharacters = require("./internal/controlCharacters.js");
48
+ var import_scorerLabel = require("./internal/scorerLabel.js");
49
+ var import_tupleKey = require("./internal/tupleKey.js");
50
+ var Trials = __toESM(require("./Trials.js"), 1);
51
+ class Runner extends Context.Service()("flows/evals/Runner") {
52
+ }
53
+ const maxReasonLength = 2048;
54
+ const boundedReason = (reason) => reason.length <= maxReasonLength ? reason : `${reason.slice(0, maxReasonLength)}[truncated]`;
55
+ const scorerKeyOf = (binding) => binding.scorer.scorerKey;
56
+ const scorerNameOf = (binding) => binding.scorer.name;
57
+ const label = (binding) => (0, import_scorerLabel.scorerLabel)({ scorer: scorerKeyOf(binding), scorerName: scorerNameOf(binding) });
58
+ const inconclusive = (request, reason, at) => {
59
+ return {
60
+ case: request.case,
61
+ scorer: scorerKeyOf(request.binding),
62
+ scorerName: scorerNameOf(request.binding),
63
+ stepKey: request.stepKey,
64
+ kind: "inconclusive",
65
+ reason,
66
+ at
67
+ };
68
+ };
69
+ const casePath = (name) => `cases['${name}']`;
70
+ const caseError = (suiteCase, cause) => {
71
+ const reason = Cause.squash(cause);
72
+ return reason instanceof import_EvalError.EvalError ? new import_EvalError.EvalError({
73
+ code: reason.code,
74
+ message: (0, import_controlCharacters.flattenControlCharacters)(`Target failed for case '${suiteCase.name}': ${reason.message}`),
75
+ path: reason.path ?? casePath(suiteCase.name),
76
+ cause: reason
77
+ }) : new import_EvalError.EvalError({
78
+ code: "executor",
79
+ message: (0, import_controlCharacters.flattenControlCharacters)(`Target failed for case '${suiteCase.name}': ${String(reason)}`),
80
+ path: casePath(suiteCase.name),
81
+ cause: reason
82
+ });
83
+ };
84
+ const runCase = (executor, suiteCase) => Effect.suspend(() => executor.run(structuredClone(suiteCase))).pipe(
85
+ Effect.map((execution) => ({
86
+ case: suiteCase.name,
87
+ execution: { ...execution, stepKey: (0, import_controlCharacters.flattenControlCharacters)(execution.stepKey) },
88
+ observations: []
89
+ })),
90
+ Effect.catchCause(
91
+ (cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.succeed({ case: suiteCase.name, error: caseError(suiteCase, cause), observations: [] })
92
+ ),
93
+ Effect.withSpan("Eval.case", { attributes: { case: suiteCase.name } })
94
+ );
95
+ const groundTruthFor = (suiteCase, binding) => {
96
+ const groundTruth = suiteCase.expected !== void 0 ? suiteCase.expected : binding.groundTruth;
97
+ return groundTruth === void 0 ? {} : { groundTruth: structuredClone(groundTruth) };
98
+ };
99
+ const requestFor = (suiteCase, execution, binding) => ({
100
+ case: suiteCase.name,
101
+ stepKey: execution.stepKey,
102
+ binding: {
103
+ ...structuredClone({
104
+ sampling: binding.sampling,
105
+ ...binding.groundTruth === void 0 ? {} : { groundTruth: binding.groundTruth },
106
+ ...binding.context === void 0 ? {} : { context: binding.context }
107
+ }),
108
+ scorer: binding.scorer,
109
+ appliesTo: binding.appliesTo
110
+ },
111
+ input: {
112
+ input: structuredClone(suiteCase.input),
113
+ output: execution.output,
114
+ ...groundTruthFor(suiteCase, binding),
115
+ ...binding.context === void 0 ? {} : { context: structuredClone(binding.context) },
116
+ latencyMs: execution.latencyMs
117
+ }
118
+ });
119
+ const observationFor = (request, result, at) => {
120
+ const source = result;
121
+ const kind = source.kind;
122
+ if (kind !== "score" && kind !== "inconclusive") {
123
+ return inconclusive(
124
+ request,
125
+ boundedReason(`Scorer ${label(request.binding)} returned an unusable observation kind '${String(kind)}'`),
126
+ at
127
+ );
128
+ }
129
+ const reason = source.reason;
130
+ if (kind === "inconclusive") {
131
+ return inconclusive(
132
+ request,
133
+ typeof reason === "string" && reason.length > 0 ? boundedReason(reason) : boundedReason(`Scorer ${label(request.binding)} returned an inconclusive observation with no reason`),
134
+ at
135
+ );
136
+ }
137
+ const score2 = source.score;
138
+ if (typeof score2 !== "number" || !Number.isFinite(score2) || score2 < 0 || score2 > 1) {
139
+ return inconclusive(
140
+ request,
141
+ boundedReason(`Scorer ${label(request.binding)} returned a score outside [0, 1]: ${String(score2)}`),
142
+ at
143
+ );
144
+ }
145
+ const meta = source.meta;
146
+ return {
147
+ case: request.case,
148
+ scorer: scorerKeyOf(request.binding),
149
+ scorerName: scorerNameOf(request.binding),
150
+ stepKey: request.stepKey,
151
+ kind: "score",
152
+ score: score2,
153
+ ...typeof reason === "string" ? { reason: boundedReason(reason) } : {},
154
+ ...meta === void 0 ? {} : { meta },
155
+ at
156
+ };
157
+ };
158
+ const executeInline = (job) => job.score.pipe(
159
+ Effect.flatMap(Scorer.validate),
160
+ Effect.map((result) => ({
161
+ ...job.observation,
162
+ kind: "score",
163
+ score: result.score,
164
+ ...result.reason === void 0 ? {} : { reason: result.reason },
165
+ ...result.meta === void 0 ? {} : { meta: result.meta },
166
+ at: job.at
167
+ })),
168
+ Effect.catchCause(
169
+ (cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.succeed(ScorerRunner.inconclusive(job, cause))
170
+ )
171
+ );
172
+ const makeInline = () => {
173
+ const runBatchCorrelated = (jobs, options) => Effect.forEach(
174
+ jobs,
175
+ (job) => executeInline(job).pipe(Effect.map((observation) => ({ identity: job.identity, observation }))),
176
+ { concurrency: options?.concurrency ?? 1 }
177
+ );
178
+ return {
179
+ runBatchCorrelated,
180
+ runBatch: (jobs, options) => runBatchCorrelated(jobs, options).pipe(Effect.map((results) => results.map((result) => result.observation)))
181
+ };
182
+ };
183
+ const layerInline = Layer.succeed(Runner)(makeInline());
184
+ const buildJobs = (cases, suite, { at, runId, sampleId, concurrency }) => Effect.gen(function* () {
185
+ const candidates = cases.flatMap((caseResult, index) => {
186
+ const execution = caseResult.execution;
187
+ if (execution === void 0) return [];
188
+ const suiteCase = suite.cases[index];
189
+ return suite.bindings.flatMap(
190
+ (binding, bindingIndex) => binding.appliesTo === execution.target ? [{ request: requestFor(suiteCase, execution, binding), bindingIndex }] : []
191
+ );
192
+ });
193
+ const sampled = yield* Effect.forEach(
194
+ candidates,
195
+ ({ request, bindingIndex }) => Sampling.decide(
196
+ request.binding.sampling,
197
+ request.stepKey,
198
+ scorerKeyOf(request.binding)
199
+ ).pipe(
200
+ Effect.map((selected) => selected ? request : void 0),
201
+ Effect.mapError(
202
+ (cause) => new import_EvalError.EvalError({
203
+ code: "invalid_suite",
204
+ message: `Invalid sampling policy for scorer ${label(request.binding)}`,
205
+ path: `bindings[${bindingIndex}].sampling`,
206
+ cause
207
+ })
208
+ )
209
+ ),
210
+ { concurrency }
211
+ );
212
+ const requests = sampled.filter((request) => request !== void 0);
213
+ const jobs = requests.map((request, index) => ({
214
+ identity: (0, import_tupleKey.tupleKey)(
215
+ suite.name,
216
+ runId,
217
+ sampleId,
218
+ request.case,
219
+ request.stepKey,
220
+ scorerKeyOf(request.binding),
221
+ `${index}`
222
+ ),
223
+ observation: { targetStepKey: request.stepKey, scorerKey: scorerKeyOf(request.binding) },
224
+ score: Effect.suspend(() => request.binding.scorer.score(request.input)),
225
+ at: Date.parse(at)
226
+ }));
227
+ return { requests, jobs };
228
+ });
229
+ const batchFallback = (jobs, cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.succeed(jobs.map((job) => ScorerRunner.inconclusive(job, cause)));
230
+ const scoreCorrelated = (jobs, scorer, runBatchCorrelated, { concurrency }) => Effect.gen(function* () {
231
+ const batchResults = yield* Effect.suspend(() => runBatchCorrelated.call(scorer, jobs, { concurrency })).pipe(
232
+ Effect.catchCause(
233
+ (cause) => batchFallback(jobs, cause).pipe(
234
+ Effect.map(
235
+ (observations) => observations.map((observation, index) => ({
236
+ identity: jobs[index].identity,
237
+ observation
238
+ }))
239
+ )
240
+ )
241
+ )
242
+ );
243
+ if (batchResults.length !== jobs.length) {
244
+ return yield* Effect.fail(
245
+ new import_EvalError.EvalError({
246
+ code: "scorer_protocol",
247
+ message: `Correlated scorer batch returned ${batchResults.length} results for ${jobs.length} jobs; a batch runner must return exactly one result per job identity`,
248
+ path: "runBatchCorrelated"
249
+ })
250
+ );
251
+ }
252
+ const jobIndexByIdentity = new Map(jobs.map((job, index) => [job.identity, index]));
253
+ const byIdentity = /* @__PURE__ */ new Map();
254
+ let unknown;
255
+ for (const [index, result] of batchResults.entries()) {
256
+ const jobIndex = jobIndexByIdentity.get(result.identity);
257
+ if (jobIndex !== void 0 && byIdentity.has(result.identity)) {
258
+ return yield* Effect.fail(
259
+ new import_EvalError.EvalError({
260
+ code: "scorer_protocol",
261
+ message: `Correlated scorer batch returned duplicate identity '${result.identity}' for job ${jobIndex} at result index ${index}`,
262
+ path: `runBatchCorrelated[${index}]`
263
+ })
264
+ );
265
+ }
266
+ if (jobIndex === void 0) unknown = { identity: result.identity, index };
267
+ byIdentity.set(result.identity, result.observation);
268
+ }
269
+ if (unknown !== void 0) {
270
+ const missingJobIndex = jobs.findIndex((job) => !byIdentity.has(job.identity));
271
+ const missingIdentity = jobs[missingJobIndex].identity;
272
+ return yield* Effect.fail(
273
+ new import_EvalError.EvalError({
274
+ code: "scorer_protocol",
275
+ message: `Correlated scorer batch returned unknown identity '${unknown.identity}' at result index ${unknown.index}; job ${missingJobIndex} identity '${missingIdentity}' is absent`,
276
+ path: `runBatchCorrelated[${unknown.index}]`
277
+ })
278
+ );
279
+ }
280
+ return jobs.map((job) => byIdentity.get(job.identity));
281
+ });
282
+ const scoreOrderOnly = (jobs, requests, scorer, { concurrency }) => Effect.gen(function* () {
283
+ const pairIndexes = /* @__PURE__ */ new Map();
284
+ for (const [index, job] of jobs.entries()) {
285
+ const pair = (0, import_tupleKey.tupleKey)(job.observation.targetStepKey, job.observation.scorerKey);
286
+ const previous = pairIndexes.get(pair);
287
+ if (previous !== void 0) {
288
+ const first = requests[previous];
289
+ const second = requests[index];
290
+ return yield* Effect.fail(
291
+ new import_EvalError.EvalError({
292
+ code: "ambiguous_score_job",
293
+ message: `Cannot attribute score jobs for cases '${first.case}' and '${second.case}': both use step key '${job.observation.targetStepKey}' and scorer ${label(second.binding)}. Give each case its own step key, or provide a batch runner that implements runBatchCorrelated`,
294
+ path: "runBatch"
295
+ })
296
+ );
297
+ }
298
+ pairIndexes.set(pair, index);
299
+ }
300
+ const results = yield* Effect.suspend(() => scorer.runBatch(jobs, { concurrency })).pipe(
301
+ Effect.catchCause((cause) => batchFallback(jobs, cause))
302
+ );
303
+ if (results.length !== jobs.length) {
304
+ return yield* Effect.fail(
305
+ new import_EvalError.EvalError({
306
+ code: "scorer_protocol",
307
+ message: `Scorer batch returned ${results.length} observations for ${jobs.length} jobs; a batch runner must return exactly one observation per job, in order`,
308
+ path: "runBatch"
309
+ })
310
+ );
311
+ }
312
+ return results;
313
+ });
314
+ const score = (cases, suite, scorer, context) => Effect.gen(function* () {
315
+ const { requests, jobs } = yield* buildJobs(cases, suite, context);
316
+ if (requests.length === 0) return cases;
317
+ const runBatchCorrelated = scorer.runBatchCorrelated;
318
+ const protocol = runBatchCorrelated === void 0 ? "runBatch" : "runBatchCorrelated";
319
+ const results = yield* runBatchCorrelated === void 0 ? scoreOrderOnly(jobs, requests, scorer, context) : scoreCorrelated(jobs, scorer, runBatchCorrelated, context);
320
+ const { at } = context;
321
+ const observations = [];
322
+ for (const [index, request] of requests.entries()) {
323
+ const result = results[index];
324
+ if (result.targetStepKey !== request.stepKey || result.scorerKey !== scorerKeyOf(request.binding)) {
325
+ return yield* Effect.fail(
326
+ new import_EvalError.EvalError({
327
+ code: "scorer_protocol",
328
+ message: protocol === "runBatchCorrelated" ? `Correlated scorer batch identity '${jobs[index].identity}' returned step '${result.targetStepKey}' and scorer '${result.scorerKey}' where job ${index} asked for scorer '${scorerKeyOf(request.binding)}' at step '${request.stepKey}'` : `Scorer batch returned an observation for '${result.scorerKey}' at step '${result.targetStepKey}' where job ${index} asked for '${scorerKeyOf(request.binding)}' at step '${request.stepKey}'; results must stay aligned with their jobs`,
329
+ path: `${protocol}[${index}]`
330
+ })
331
+ );
332
+ }
333
+ observations.push(observationFor(request, result, at));
334
+ }
335
+ const byCase = /* @__PURE__ */ new Map();
336
+ for (const observation of observations) {
337
+ const bucket = byCase.get(observation.case);
338
+ if (bucket === void 0) byCase.set(observation.case, [observation]);
339
+ else bucket.push(observation);
340
+ }
341
+ return cases.map((caseResult) => ({
342
+ ...caseResult,
343
+ observations: byCase.get(caseResult.case) ?? []
344
+ }));
345
+ });
346
+ const run = (suite, options) => Effect.gen(function* () {
347
+ const executor = yield* import_CaseExecutor.CaseExecutor;
348
+ const injected = yield* Effect.serviceOption(Runner);
349
+ const scorer = options.scorer ?? (injected._tag === "Some" ? injected.value : makeInline());
350
+ const atMillis = Date.parse(options.at);
351
+ if (options.runId.trim().length === 0) {
352
+ return yield* Effect.fail(
353
+ new import_EvalError.EvalError({
354
+ code: "invalid_run_options",
355
+ message: "Deterministic runs require a non-empty runId",
356
+ path: "options.runId"
357
+ })
358
+ );
359
+ }
360
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(options.at) || !Number.isFinite(atMillis) || new Date(atMillis).toISOString() !== options.at) {
361
+ return yield* Effect.fail(
362
+ new import_EvalError.EvalError({
363
+ code: "invalid_run_options",
364
+ message: `Deterministic runs require a canonical UTC timestamp such as 2026-01-01T00:00:00.000Z, got '${options.at}'`,
365
+ path: "options.at"
366
+ })
367
+ );
368
+ }
369
+ const n = options.trials ?? suite.trials ?? 1;
370
+ const k = options.k ?? 1;
371
+ if (!Number.isSafeInteger(n) || n < 1 || n > 1e3 || !Number.isSafeInteger(k) || k < 1 || k > n) {
372
+ return yield* Effect.fail(
373
+ new import_EvalError.EvalError({
374
+ code: "invalid_trials",
375
+ message: "trials must be an integer from 1 to 1000 and k must be an integer from 1 to trials",
376
+ path: "options.trials"
377
+ })
378
+ );
379
+ }
380
+ const at = options.at;
381
+ const runId = options.runId;
382
+ const sampleId = options.sampleId ?? "default";
383
+ const batches = yield* Effect.forEach(Array.from({ length: n }, (_, index) => index), (index) => Effect.gen(function* () {
384
+ const cases2 = yield* Effect.forEach(suite.cases, (suiteCase) => runCase(executor, suiteCase), {
385
+ concurrency: suite.concurrency,
386
+ discard: false
387
+ });
388
+ return yield* score(cases2, suite, scorer, {
389
+ concurrency: suite.concurrency,
390
+ at,
391
+ runId,
392
+ sampleId: n === 1 ? sampleId : (0, import_tupleKey.tupleKey)(sampleId, String(index))
393
+ });
394
+ }), { concurrency: 1 });
395
+ const outcomes = {};
396
+ const cases = suite.cases.map((suiteCase, index) => {
397
+ const attempts = batches.map(
398
+ (batch) => batch[index]
399
+ );
400
+ const passed = attempts.map(
401
+ (attempt) => attempt.error === void 0 && attempt.observations.length > 0 && attempt.observations.every((observation) => observation.kind === "score" && observation.score === 1)
402
+ );
403
+ outcomes[suiteCase.name] = passed;
404
+ return {
405
+ ...attempts[0],
406
+ error: attempts.find((attempt) => attempt.error !== void 0)?.error,
407
+ observations: attempts.flatMap((attempt) => attempt.observations),
408
+ trials: Trials.summarize(passed, k)
409
+ };
410
+ });
411
+ return {
412
+ runId,
413
+ suite: suite.name,
414
+ cases,
415
+ observations: cases.flatMap((caseResult) => caseResult.observations),
416
+ trials: Trials.aggregate(outcomes, k),
417
+ k
418
+ };
419
+ }).pipe(Effect.withSpan("Eval.run", { attributes: { suite: suite.name, runId: options.runId } }));
420
+ const layerNoop = Layer.sync(Runner)(() => {
421
+ const unavailable = new import_EvalError.EvalError({ code: "scorer_unavailable", message: "No scorer batch runner is available" });
422
+ return {
423
+ runBatch: () => Effect.fail(unavailable),
424
+ runBatchCorrelated: (jobs) => Effect.succeed(jobs.map((job) => ({
425
+ identity: job.identity,
426
+ observation: ScorerRunner.inconclusive(job, unavailable)
427
+ })))
428
+ };
429
+ });
430
+ //# sourceMappingURL=Runner.js.map