@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,25 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var tupleKey_exports = {};
20
+ __export(tupleKey_exports, {
21
+ tupleKey: () => tupleKey
22
+ });
23
+ module.exports = __toCommonJS(tupleKey_exports);
24
+ const tupleKey = (...parts) => JSON.stringify(parts);
25
+ //# sourceMappingURL=tupleKey.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/internal/tupleKey.ts"],
4
+ "sourcesContent": ["/**\n * The one injective tuple encoder every key in this package is built with.\n *\n * Joining caller-supplied strings on a delimiter is not injective: under a NUL\n * separator the tuples `[\"a\", \"b\" + NUL + \"c\"]` and `[\"a\" + NUL + \"b\", \"c\"]`\n * produce one key, so two distinct score jobs could share an identity and two\n * distinct baseline records could sort as one. Encoding the tuple as a JSON\n * array keeps the components separated by the encoding itself, whatever they\n * contain.\n *\n * @since 0.1.0\n */\n\n/**\n * Encodes a tuple of strings as one key.\n *\n * @since 0.1.0\n * @private\n */\nexport const tupleKey = (...parts: ReadonlyArray<string>): string => JSON.stringify(parts)\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBO,MAAM,WAAW,IAAI,UAAyC,KAAK,UAAU,KAAK;",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -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,203 @@
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.js";
13
+ import { CanonicalJson } from "./internal/CanonicalJson.js";
14
+ import { controlCharacter } from "./internal/controlCharacters.js";
15
+ import { tupleKey } from "./internal/tupleKey.js";
16
+ /**
17
+ * Current committed baseline artifact version.
18
+ *
19
+ * @category models
20
+ * @since 0.1.0
21
+ */
22
+ export const version = 1;
23
+ /**
24
+ * The declared ceilings a committed baseline is validated against.
25
+ *
26
+ * `length` bounds the JSON text `load` will parse, so an oversized committed
27
+ * file fails with a sentence instead of exhausting a CI runner's memory.
28
+ *
29
+ * @category models
30
+ * @since 0.1.0
31
+ */
32
+ export const limits = {
33
+ /** Longest baseline JSON text, in UTF-16 code units. */
34
+ length: 16 * 1024 * 1024
35
+ };
36
+ const fail = (message, path) => Effect.fail(new EvalError({ code: "invalid_baseline", message, ...(path === undefined ? {} : { path }) }));
37
+ // A committed record is read by a gate whose summary goes to a CI log, so it
38
+ // follows the same control-character rule as a suite name: rejected where it
39
+ // enters the system, not flattened later.
40
+ const stringField = (value, field, path) => {
41
+ if (typeof value !== "string") {
42
+ return fail(`Baseline record field '${field}' must be a string, got ${typeof value}`, path);
43
+ }
44
+ const control = controlCharacter(value);
45
+ return control === undefined
46
+ ? Effect.succeed(value)
47
+ : fail(`Baseline record field '${field}' must not contain the control character ${control}`, path);
48
+ };
49
+ const decodeRecord = (value, index) => Effect.gen(function* () {
50
+ const at = `records[${index}]`;
51
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
52
+ return yield* fail(`Baseline record must be an object, got ${value === null ? "null" : typeof value}`, at);
53
+ }
54
+ const source = value;
55
+ const rawSuite = source.suite;
56
+ const rawCase = source.case;
57
+ const rawScorer = source.scorer;
58
+ const rawScorerName = source.scorerName;
59
+ const rawStepKey = source.stepKey;
60
+ const rawScore = source.score;
61
+ const suite = yield* stringField(rawSuite, "suite", `${at}.suite`);
62
+ const caseName = yield* stringField(rawCase, "case", `${at}.case`);
63
+ const scorer = yield* stringField(rawScorer, "scorer", `${at}.scorer`);
64
+ const stepKey = yield* stringField(rawStepKey, "stepKey", `${at}.stepKey`);
65
+ const score = rawScore;
66
+ if (typeof score !== "number" || !Number.isFinite(score) || score < 0 || score > 1) {
67
+ return yield* fail(`Baseline record field 'score' must be a finite number in [0, 1], got ${String(score)}`, `${at}.score`);
68
+ }
69
+ const scorerName = rawScorerName === undefined
70
+ ? undefined
71
+ : yield* stringField(rawScorerName, "scorerName", `${at}.scorerName`);
72
+ // Every record is rebuilt field by field. Keeping the caller's object would
73
+ // carry unknown keys, and any getter among them, straight into a committed
74
+ // artifact.
75
+ return {
76
+ suite,
77
+ case: caseName,
78
+ scorer,
79
+ ...(scorerName === undefined ? {} : { scorerName }),
80
+ stepKey,
81
+ score: Object.is(score, -0) ? 0 : score
82
+ };
83
+ });
84
+ const validate = (value) => Effect.gen(function* () {
85
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
86
+ return yield* fail("Baseline must be an object");
87
+ }
88
+ const artifact = value;
89
+ const rawVersion = artifact.version;
90
+ const rawSuite = artifact.suite;
91
+ const rawRecords = artifact.records;
92
+ if (rawVersion !== version) {
93
+ return yield* fail(`Baseline version must be ${version}, got ${String(rawVersion)}`, "version");
94
+ }
95
+ if (rawSuite !== undefined && typeof rawSuite !== "string") {
96
+ return yield* fail(`Baseline field 'suite' must be a string, got ${typeof rawSuite}`, "suite");
97
+ }
98
+ const suiteControl = rawSuite === undefined ? undefined : controlCharacter(rawSuite);
99
+ if (suiteControl !== undefined) {
100
+ return yield* fail(`Baseline field 'suite' must not contain the control character ${suiteControl}`, "suite");
101
+ }
102
+ if (!Array.isArray(rawRecords)) {
103
+ return yield* fail("Baseline records must be an array", "records");
104
+ }
105
+ const records = yield* Effect.forEach(rawRecords, decodeRecord);
106
+ let suite = rawSuite;
107
+ if (suite === undefined) {
108
+ const first = records[0];
109
+ if (first === undefined) {
110
+ return yield* fail("Cannot infer baseline suite: legacy artifact has no records", "suite");
111
+ }
112
+ if (records.some((record) => record.suite !== first.suite)) {
113
+ return yield* fail("Cannot infer baseline suite: legacy records name multiple suites", "suite");
114
+ }
115
+ suite = first.suite;
116
+ }
117
+ return { version, suite, records: Object.freeze(records) };
118
+ });
119
+ /**
120
+ * Builds and validates a baseline from a run's successful observations.
121
+ *
122
+ * Inconclusive observations are dropped: a baseline records what was measured,
123
+ * and an inconclusive observation measured nothing.
124
+ *
125
+ * @category constructors
126
+ * @since 0.1.0
127
+ */
128
+ export const fromRun = (run) => {
129
+ const records = run.observations.flatMap((observation) => observation.kind === "score"
130
+ ? [{
131
+ suite: run.suite,
132
+ case: observation.case,
133
+ scorer: observation.scorer,
134
+ ...(observation.scorerName === undefined ? {} : { scorerName: observation.scorerName }),
135
+ stepKey: observation.stepKey,
136
+ score: observation.score
137
+ }]
138
+ : []);
139
+ return validate({ version, suite: run.suite, records });
140
+ };
141
+ /**
142
+ * Validates an in-memory baseline.
143
+ *
144
+ * The result is a snapshot: every known field is read once, then records are
145
+ * rebuilt from those validated values and the array is frozen. Mutating the
146
+ * array or the objects that were passed in cannot change the validated
147
+ * baseline, and a stateful getter cannot substitute a value after validation.
148
+ *
149
+ * Fails with `invalid_baseline` carrying the record index and field name in
150
+ * `path` for a wrong version, a non-array `records`, a record that is not an
151
+ * object, an identity field that is not a string or holds a control
152
+ * character, or a score that is not finite in `[0, 1]`.
153
+ *
154
+ * @category constructors
155
+ * @since 0.1.0
156
+ */
157
+ export const make = (baseline) => {
158
+ const artifactVersion = baseline.version ?? version;
159
+ const records = baseline.records;
160
+ return validate({ version: artifactVersion, suite: baseline.suite, records });
161
+ };
162
+ /**
163
+ * Serializes a baseline with recursively sorted keys and stable numbers.
164
+ *
165
+ * Records are ordered by an injective encoding of `(suite, case, scorer,
166
+ * stepKey)`, so two records whose names differ only in where a separator falls
167
+ * still sort apart. The result ends with a newline and never throws: a
168
+ * validated baseline holds only strings and finite numbers.
169
+ *
170
+ * @category serialization
171
+ * @since 0.1.0
172
+ */
173
+ export const write = (baseline) => {
174
+ const sortKey = (record) => tupleKey(record.suite, record.case, record.scorer, record.stepKey);
175
+ const records = baseline.records
176
+ .map((record) => [sortKey(record), record])
177
+ .sort(([left], [right]) => CanonicalJson.compareText(left, right))
178
+ .map(([, record]) => record);
179
+ return CanonicalJson.stringify({
180
+ version: baseline.version,
181
+ suite: baseline.suite,
182
+ records
183
+ });
184
+ };
185
+ /**
186
+ * Loads and validates baseline JSON.
187
+ *
188
+ * A version-1 artifact without a top-level `suite` is accepted only when its
189
+ * nonempty records all name the same suite. That suite becomes the artifact's
190
+ * owner, so writing the result includes it. Empty or ambiguous legacy artifacts
191
+ * fail with `invalid_baseline` at `suite`. Text longer than `limits.length`
192
+ * fails with `invalid_baseline` at `text` before any of it is parsed.
193
+ *
194
+ * @category serialization
195
+ * @since 0.1.0
196
+ */
197
+ export const load = (text) => text.length > limits.length
198
+ ? fail(`Baseline must be at most ${limits.length} characters, got ${text.length}`, "text")
199
+ : Effect.try({
200
+ try: () => JSON.parse(text),
201
+ catch: (cause) => new EvalError({ code: "invalid_baseline", message: "Baseline is not valid JSON", cause })
202
+ }).pipe(Effect.flatMap(validate));
203
+ //# sourceMappingURL=Baseline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Baseline.js","sourceRoot":"","sources":["../../src/Baseline.ts"],"names":[],"mappings":"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","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"]}
@@ -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"}
@@ -0,0 +1,60 @@
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 * as Context from "effect/Context";
12
+ import * as Effect from "effect/Effect";
13
+ import * as Layer from "effect/Layer";
14
+ import { EvalError } from "./EvalError.js";
15
+ /**
16
+ * Injectable execution boundary for a target flow.
17
+ *
18
+ * @category services
19
+ * @since 0.1.0
20
+ */
21
+ export class CaseExecutor extends Context.Service()("flows/evals/CaseExecutor") {
22
+ }
23
+ /**
24
+ * Builds an executor from its one callback.
25
+ *
26
+ * Throws a `TypeError` when there is no callback. An executor that silently
27
+ * degraded to {@link makeNoop} turned one wiring mistake into a whole suite of
28
+ * cases failing with `executor`, which reads as a broken target rather than a
29
+ * missing one.
30
+ *
31
+ * @category constructors
32
+ * @since 0.1.0
33
+ */
34
+ export const make = (run) => {
35
+ if (typeof run !== "function") {
36
+ throw new TypeError("CaseExecutor.make needs a callback");
37
+ }
38
+ return CaseExecutor.of({ run });
39
+ };
40
+ /**
41
+ * Builds an executor that fails every case with a typed executor error.
42
+ *
43
+ * @category constructors
44
+ * @since 0.1.0
45
+ */
46
+ export const makeNoop = () => CaseExecutor.of({
47
+ run: (suiteCase) => Effect.fail(new EvalError({
48
+ code: "executor",
49
+ message: `No executor is available for case '${suiteCase.name}'`,
50
+ path: `cases['${suiteCase.name}']`
51
+ }))
52
+ });
53
+ /**
54
+ * Provides the unavailable executor.
55
+ *
56
+ * @category layers
57
+ * @since 0.1.0
58
+ */
59
+ export const layerNoop = Layer.succeed(CaseExecutor)(makeNoop());
60
+ //# sourceMappingURL=CaseExecutor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CaseExecutor.js","sourceRoot":"","sources":["../../src/CaseExecutor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,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;AAsC1C;;;;;GAKG;AACH,MAAM,OAAO,YAAa,SAAQ,OAAO,CAAC,OAAO,EAAyB,CAAC,0BAA0B,CAAC;CAAG;AAEzG;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,GAAQ,EAAW,EAAE;IACxC,IAAI,OAAO,GAAG,KAAK,UAAU,EAAE,CAAC;QAC9B,MAAM,IAAI,SAAS,CAAC,oCAAoC,CAAC,CAAA;IAC3D,CAAC;IACD,OAAO,YAAY,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;AACjC,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAY,EAAE,CACpC,YAAY,CAAC,EAAE,CAAC;IACd,GAAG,EAAE,CAAC,SAAS,EAAE,EAAE,CACjB,MAAM,CAAC,IAAI,CACT,IAAI,SAAS,CAAC;QACZ,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,sCAAsC,SAAS,CAAC,IAAI,GAAG;QAChE,IAAI,EAAE,UAAU,SAAS,CAAC,IAAI,IAAI;KACnC,CAAC,CACH;CACJ,CAAC,CAAA;AAEJ;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAA8B,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAA","sourcesContent":["/**\n * Injectable target-flow execution boundary.\n *\n * A suite says what to run; this service says how. Keeping it injectable is\n * what lets the same suite run against a real flow, a scripted host, or nothing\n * at all, and it is the only place a target's failure is converted into a typed\n * evaluation failure.\n *\n * @since 0.1.0\n */\n\nimport type { Flow } from \"@smthrs/core\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport { EvalError } from \"./EvalError.ts\"\nimport type { Case } from \"./Suite.ts\"\n\n/**\n * The result of executing one target-flow case.\n *\n * `target` is the flow value the case actually executed. A run matches it\n * against each binding's `appliesTo` by reference identity, so it has to be the\n * declared flow itself rather than a copy of it.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Execution {\n readonly output: unknown\n readonly stepKey: string\n readonly latencyMs: number\n readonly target: Pick<Flow.Any, \"name\">\n}\n\n/**\n * The one callback a case executor is.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Run = (suiteCase: Case) => Effect.Effect<Execution, EvalError>\n\n/**\n * Runtime shape for an injectable target-flow executor.\n *\n * @category services\n * @since 0.1.0\n */\nexport interface Service {\n readonly run: Run\n}\n\n/**\n * Injectable execution boundary for a target flow.\n *\n * @category services\n * @since 0.1.0\n */\nexport class CaseExecutor extends Context.Service<CaseExecutor, Service>()(\"flows/evals/CaseExecutor\") {}\n\n/**\n * Builds an executor from its one callback.\n *\n * Throws a `TypeError` when there is no callback. An executor that silently\n * degraded to {@link makeNoop} turned one wiring mistake into a whole suite of\n * cases failing with `executor`, which reads as a broken target rather than a\n * missing one.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (run: Run): Service => {\n if (typeof run !== \"function\") {\n throw new TypeError(\"CaseExecutor.make needs a callback\")\n }\n return CaseExecutor.of({ run })\n}\n\n/**\n * Builds an executor that fails every case with a typed executor error.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeNoop = (): Service =>\n CaseExecutor.of({\n run: (suiteCase) =>\n Effect.fail(\n new EvalError({\n code: \"executor\",\n message: `No executor is available for case '${suiteCase.name}'`,\n path: `cases['${suiteCase.name}']`\n })\n )\n })\n\n/**\n * Provides the unavailable executor.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerNoop: Layer.Layer<CaseExecutor> = Layer.succeed(CaseExecutor)(makeNoop())\n"]}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The typed failures evaluation raises, with the stable codes a gate or a
3
+ * report branches on.
4
+ *
5
+ * @since 0.1.0
6
+ */
7
+ import * as Schema from "effect/Schema";
8
+ /**
9
+ * Stable evaluation failure codes.
10
+ *
11
+ * Each code names one thing a caller can do about it, so a gate that branches
12
+ * on the code routes the failure to the right owner:
13
+ *
14
+ * - `invalid_suite`: the suite declaration is wrong. Fix the suite.
15
+ * - `invalid_run_options`: `Runner.run`'s own options are wrong, not the suite.
16
+ * - `invalid_baseline`: the committed baseline is unreadable, belongs to
17
+ * another suite, or holds a record the schema rejects. Regenerate it.
18
+ * - `invalid_tolerance`: the caller passed a tolerance that is not a finite
19
+ * non-negative number. Fix the call, not the baseline.
20
+ * - `invalid_trials`: the caller passed run, pass, or `k` counts to `Trials`
21
+ * outside their bounds. Fix the call.
22
+ * - `executor`: the target flow failed for a case.
23
+ * - `ambiguous_score_job`: two score jobs shared a step key and a scorer, so
24
+ * an order-only batch runner's results cannot be attributed to a case. Give
25
+ * each case its own step key, or use a runner that implements
26
+ * `runBatchCorrelated`.
27
+ * - `scorer_protocol`: a batch runner broke the `runBatch` contract by returning
28
+ * the wrong number of observations, or observations that identify jobs other
29
+ * than the ones it was given. Nothing it returned can be trusted.
30
+ * - `scorer_unavailable`: no batch runner was available to score with.
31
+ *
32
+ * @category models
33
+ * @since 0.1.0
34
+ */
35
+ export declare const EvalErrorCode: Schema.Literals<readonly ["invalid_suite", "invalid_run_options", "invalid_baseline", "invalid_tolerance", "invalid_trials", "executor", "ambiguous_score_job", "scorer_protocol", "scorer_unavailable"]>;
36
+ /**
37
+ * Stable evaluation failure code.
38
+ *
39
+ * @category models
40
+ * @since 0.1.0
41
+ */
42
+ export type EvalErrorCode = typeof EvalErrorCode.Type;
43
+ declare const EvalError_base: Schema.Class<EvalError, Schema.TaggedStruct<"flows/evals/EvalError", {
44
+ readonly code: Schema.Literals<readonly ["invalid_suite", "invalid_run_options", "invalid_baseline", "invalid_tolerance", "invalid_trials", "executor", "ambiguous_score_job", "scorer_protocol", "scorer_unavailable"]>;
45
+ readonly message: Schema.String;
46
+ readonly path: Schema.optional<Schema.String>;
47
+ readonly cause: Schema.optional<Schema.Defect>;
48
+ }>, import("effect/Cause").YieldableError>;
49
+ /**
50
+ * A typed failure raised while loading or executing an evaluation.
51
+ *
52
+ * `code` is the stable branch point, `message` is the sentence a CI log shows,
53
+ * and `path` locates the offending value inside the input the caller supplied
54
+ * (`records[3].score`, `cases[1].input`, `options.at`). `cause` retains the
55
+ * original failure; it is never rendered on its own, so a message that matters
56
+ * to an operator has to say so itself.
57
+ *
58
+ * @category errors
59
+ * @since 0.1.0
60
+ */
61
+ export declare class EvalError extends EvalError_base {
62
+ }
63
+ export {};
64
+ //# sourceMappingURL=EvalError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EvalError.d.ts","sourceRoot":"","sources":["../../src/EvalError.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,aAAa,2MAUxB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC,IAAI,CAAA;;;;;;;AAErD;;;;;;;;;;;GAWG;AACH,qBAAa,SAAU,SAAQ,cAK7B;CAAG"}