@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.
- package/CHANGELOG.md +142 -0
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/cjs/Baseline.d.ts +118 -0
- package/dist/cjs/Baseline.d.ts.map +1 -0
- package/dist/cjs/Baseline.js +158 -0
- package/dist/cjs/Baseline.js.map +7 -0
- package/dist/cjs/CaseExecutor.d.ts +85 -0
- package/dist/cjs/CaseExecutor.d.ts.map +1 -0
- package/dist/cjs/CaseExecutor.js +59 -0
- package/dist/cjs/CaseExecutor.js.map +7 -0
- package/dist/cjs/EvalError.d.ts +64 -0
- package/dist/cjs/EvalError.d.ts.map +1 -0
- package/dist/cjs/EvalError.js +54 -0
- package/dist/cjs/EvalError.js.map +7 -0
- package/dist/cjs/Gate.d.ts +55 -0
- package/dist/cjs/Gate.d.ts.map +1 -0
- package/dist/cjs/Gate.js +88 -0
- package/dist/cjs/Gate.js.map +7 -0
- package/dist/cjs/Regression.d.ts +116 -0
- package/dist/cjs/Regression.d.ts.map +1 -0
- package/dist/cjs/Regression.js +191 -0
- package/dist/cjs/Regression.js.map +7 -0
- package/dist/cjs/Report.d.ts +114 -0
- package/dist/cjs/Report.d.ts.map +1 -0
- package/dist/cjs/Report.js +161 -0
- package/dist/cjs/Report.js.map +7 -0
- package/dist/cjs/Runner.d.ts +229 -0
- package/dist/cjs/Runner.d.ts.map +1 -0
- package/dist/cjs/Runner.js +430 -0
- package/dist/cjs/Runner.js.map +7 -0
- package/dist/cjs/Suite.d.ts +139 -0
- package/dist/cjs/Suite.d.ts.map +1 -0
- package/dist/cjs/Suite.js +221 -0
- package/dist/cjs/Suite.js.map +7 -0
- package/dist/cjs/Trials.d.ts +93 -0
- package/dist/cjs/Trials.d.ts.map +1 -0
- package/dist/cjs/Trials.js +82 -0
- package/dist/cjs/Trials.js.map +7 -0
- package/dist/cjs/index.d.ts +76 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +51 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/CanonicalJson.d.ts +44 -0
- package/dist/cjs/internal/CanonicalJson.d.ts.map +1 -0
- package/dist/cjs/internal/CanonicalJson.js +114 -0
- package/dist/cjs/internal/CanonicalJson.js.map +7 -0
- package/dist/cjs/internal/controlCharacters.d.ts +44 -0
- package/dist/cjs/internal/controlCharacters.d.ts.map +1 -0
- package/dist/cjs/internal/controlCharacters.js +42 -0
- package/dist/cjs/internal/controlCharacters.js.map +7 -0
- package/dist/cjs/internal/scorerLabel.d.ts +33 -0
- package/dist/cjs/internal/scorerLabel.d.ts.map +1 -0
- package/dist/cjs/internal/scorerLabel.js +28 -0
- package/dist/cjs/internal/scorerLabel.js.map +7 -0
- package/dist/cjs/internal/tupleKey.d.ts +20 -0
- package/dist/cjs/internal/tupleKey.d.ts.map +1 -0
- package/dist/cjs/internal/tupleKey.js +25 -0
- package/dist/cjs/internal/tupleKey.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/Baseline.d.ts +118 -0
- package/dist/esm/Baseline.d.ts.map +1 -0
- package/dist/esm/Baseline.js +203 -0
- package/dist/esm/Baseline.js.map +1 -0
- package/dist/esm/CaseExecutor.d.ts +85 -0
- package/dist/esm/CaseExecutor.d.ts.map +1 -0
- package/dist/esm/CaseExecutor.js +60 -0
- package/dist/esm/CaseExecutor.js.map +1 -0
- package/dist/esm/EvalError.d.ts +64 -0
- package/dist/esm/EvalError.d.ts.map +1 -0
- package/dist/esm/EvalError.js +65 -0
- package/dist/esm/EvalError.js.map +1 -0
- package/dist/esm/Gate.d.ts +55 -0
- package/dist/esm/Gate.d.ts.map +1 -0
- package/dist/esm/Gate.js +90 -0
- package/dist/esm/Gate.js.map +1 -0
- package/dist/esm/Regression.d.ts +116 -0
- package/dist/esm/Regression.d.ts.map +1 -0
- package/dist/esm/Regression.js +195 -0
- package/dist/esm/Regression.js.map +1 -0
- package/dist/esm/Report.d.ts +114 -0
- package/dist/esm/Report.d.ts.map +1 -0
- package/dist/esm/Report.js +188 -0
- package/dist/esm/Report.js.map +1 -0
- package/dist/esm/Runner.d.ts +229 -0
- package/dist/esm/Runner.d.ts.map +1 -0
- package/dist/esm/Runner.js +430 -0
- package/dist/esm/Runner.js.map +1 -0
- package/dist/esm/Suite.d.ts +139 -0
- package/dist/esm/Suite.d.ts.map +1 -0
- package/dist/esm/Suite.js +255 -0
- package/dist/esm/Suite.js.map +1 -0
- package/dist/esm/Trials.d.ts +93 -0
- package/dist/esm/Trials.d.ts.map +1 -0
- package/dist/esm/Trials.js +116 -0
- package/dist/esm/Trials.js.map +1 -0
- package/dist/esm/index.d.ts +76 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +76 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/CanonicalJson.d.ts +44 -0
- package/dist/esm/internal/CanonicalJson.d.ts.map +1 -0
- package/dist/esm/internal/CanonicalJson.js +202 -0
- package/dist/esm/internal/CanonicalJson.js.map +1 -0
- package/dist/esm/internal/controlCharacters.d.ts +44 -0
- package/dist/esm/internal/controlCharacters.d.ts.map +1 -0
- package/dist/esm/internal/controlCharacters.js +65 -0
- package/dist/esm/internal/controlCharacters.js.map +1 -0
- package/dist/esm/internal/scorerLabel.d.ts +33 -0
- package/dist/esm/internal/scorerLabel.d.ts.map +1 -0
- package/dist/esm/internal/scorerLabel.js +33 -0
- package/dist/esm/internal/scorerLabel.js.map +1 -0
- package/dist/esm/internal/tupleKey.d.ts +20 -0
- package/dist/esm/internal/tupleKey.d.ts.map +1 -0
- package/dist/esm/internal/tupleKey.js +20 -0
- package/dist/esm/internal/tupleKey.js.map +1 -0
- package/docs/README.md +130 -0
- package/docs/api.md +626 -0
- package/docs/concepts/determinism.md +64 -0
- package/docs/concepts/fixed-suites.md +71 -0
- package/docs/concepts/step-keys.md +60 -0
- package/docs/guides/author-a-suite.md +115 -0
- package/docs/guides/gate-a-run-in-ci.md +129 -0
- package/docs/guides/maintain-a-baseline.md +91 -0
- package/docs/guides/run-a-suite.md +150 -0
- package/docs/installation.md +46 -0
- package/docs/quickstart.md +238 -0
- package/docs/troubleshooting.md +148 -0
- package/package.json +198 -3
- package/src/Baseline.ts +259 -0
- package/src/CaseExecutor.ts +104 -0
- package/src/EvalError.ts +74 -0
- package/src/Gate.ts +128 -0
- package/src/Regression.ts +314 -0
- package/src/Report.ts +268 -0
- package/src/Runner.ts +759 -0
- package/src/Suite.ts +358 -0
- package/src/Trials.ts +154 -0
- package/src/index.ts +76 -0
- package/src/internal/CanonicalJson.ts +234 -0
- package/src/internal/controlCharacters.ts +66 -0
- package/src/internal/scorerLabel.ts +35 -0
- package/src/internal/tupleKey.ts +20 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed evaluation suite declarations.
|
|
3
|
+
*
|
|
4
|
+
* A suite is the fixed input half of an evaluation: named cases, the scorer
|
|
5
|
+
* bindings that grade them, and the concurrency the runner is allowed. It is
|
|
6
|
+
* validated once and then never changes, because the run that produced a
|
|
7
|
+
* committed baseline has to be reproducible from the suite value that was
|
|
8
|
+
* validated.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.1.0
|
|
11
|
+
*/
|
|
12
|
+
import type * as ScorerBinding from "@smthrs/scorers/Binding";
|
|
13
|
+
import * as Effect from "effect/Effect";
|
|
14
|
+
import { EvalError } from "./EvalError.ts";
|
|
15
|
+
/**
|
|
16
|
+
* A scorer binding accepted from `@smthrs/scorers`.
|
|
17
|
+
*
|
|
18
|
+
* The binding's `appliesTo` flow is matched against an execution's `target` by
|
|
19
|
+
* reference identity, so a binding only ever grades the exact flow value it was
|
|
20
|
+
* declared against.
|
|
21
|
+
*
|
|
22
|
+
* @category models
|
|
23
|
+
* @since 0.1.0
|
|
24
|
+
*/
|
|
25
|
+
export type Binding = ScorerBinding.Binding;
|
|
26
|
+
/**
|
|
27
|
+
* One immutable fixed-suite case.
|
|
28
|
+
*
|
|
29
|
+
* `input` is handed to the executor and `expected` is offered to a bound scorer
|
|
30
|
+
* as ground truth. A declared `expected`, including `null`, wins over the
|
|
31
|
+
* binding's `groundTruth`; only an absent one defers to the binding. Both are
|
|
32
|
+
* snapshots taken by {@link make}: mutating the object a caller passed in never
|
|
33
|
+
* changes the suite.
|
|
34
|
+
*
|
|
35
|
+
* @category models
|
|
36
|
+
* @since 0.1.0
|
|
37
|
+
*/
|
|
38
|
+
export interface Case {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly input: unknown;
|
|
41
|
+
readonly expected?: unknown | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Options for constructing a suite.
|
|
45
|
+
*
|
|
46
|
+
* @category models
|
|
47
|
+
* @since 0.1.0
|
|
48
|
+
*/
|
|
49
|
+
export interface MakeOptions {
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly cases: ReadonlyArray<Case>;
|
|
52
|
+
readonly bindings?: ReadonlyArray<Binding> | undefined;
|
|
53
|
+
readonly concurrency: number;
|
|
54
|
+
readonly trials?: number | undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A validated, named collection of fixed cases and scorer bindings.
|
|
58
|
+
*
|
|
59
|
+
* The suite, cases, bindings, and their inert data are deeply frozen snapshots.
|
|
60
|
+
* Executable scorer and target identities remain unchanged.
|
|
61
|
+
*
|
|
62
|
+
* @category models
|
|
63
|
+
* @since 0.1.0
|
|
64
|
+
*/
|
|
65
|
+
export interface Suite {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly cases: ReadonlyArray<Case>;
|
|
68
|
+
readonly bindings: ReadonlyArray<Binding>;
|
|
69
|
+
readonly concurrency: number;
|
|
70
|
+
readonly trials?: number | undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The declared ceilings a suite is validated against.
|
|
74
|
+
*
|
|
75
|
+
* They exist so a mistake fails at construction with a sentence instead of
|
|
76
|
+
* exhausting memory in a runner: `concurrency` bounds the fibers a run may hold
|
|
77
|
+
* open, `cases` bounds one suite, and `fixtureLength` bounds one JSON Lines
|
|
78
|
+
* fixture.
|
|
79
|
+
*
|
|
80
|
+
* @category models
|
|
81
|
+
* @since 0.1.0
|
|
82
|
+
*/
|
|
83
|
+
export declare const limits: {
|
|
84
|
+
readonly concurrency: 1024;
|
|
85
|
+
readonly cases: 10000;
|
|
86
|
+
/** Longest JSON Lines fixture, in UTF-16 code units. */
|
|
87
|
+
readonly fixtureLength: number;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Builds and validates a fixed suite.
|
|
91
|
+
*
|
|
92
|
+
* When the effect runs, it reads every option, case field, and binding field
|
|
93
|
+
* exactly once, then validates and copies only what it read. Validating one
|
|
94
|
+
* value and copying another let a getter hand the suite something validation
|
|
95
|
+
* never saw. Every case and binding is then copied, including a binding's
|
|
96
|
+
* sampling policy, so the suite is a snapshot the caller can no longer reach:
|
|
97
|
+
* mutating an array, case input, or ratio policy afterwards leaves the
|
|
98
|
+
* validated suite unchanged. Data is checked before `structuredClone` and
|
|
99
|
+
* deeply frozen afterwards. Only plain objects, arrays, and cloneable
|
|
100
|
+
* primitives are admitted, with enumerable string-keyed data properties.
|
|
101
|
+
* Functions, class instances, mutable built-ins, and accessors fail with
|
|
102
|
+
* `invalid_suite` naming the offending path. Cycles and shared references are
|
|
103
|
+
* supported; null-prototype records become ordinary objects in the clone.
|
|
104
|
+
*
|
|
105
|
+
* Fails with `invalid_suite` for an empty or control-character name, no cases,
|
|
106
|
+
* more than `limits.cases` cases, a duplicate case name, or a concurrency that
|
|
107
|
+
* is not a safe integer in `[1, limits.concurrency]`.
|
|
108
|
+
*
|
|
109
|
+
* @category constructors
|
|
110
|
+
* @since 0.1.0
|
|
111
|
+
*/
|
|
112
|
+
export declare const make: (options: MakeOptions) => Effect.Effect<Suite, EvalError>;
|
|
113
|
+
/**
|
|
114
|
+
* Options used when decoding JSON Lines.
|
|
115
|
+
*
|
|
116
|
+
* @category models
|
|
117
|
+
* @since 0.1.0
|
|
118
|
+
*/
|
|
119
|
+
export interface JsonLinesOptions {
|
|
120
|
+
readonly name: string;
|
|
121
|
+
readonly bindings?: ReadonlyArray<Binding> | undefined;
|
|
122
|
+
readonly concurrency: number;
|
|
123
|
+
readonly trials?: number | undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Loads the `{ name, input, expected? }` JSON Lines fixture format.
|
|
127
|
+
*
|
|
128
|
+
* Blank lines are skipped, a leading byte-order mark is stripped, and both LF
|
|
129
|
+
* and CRLF terminate a line. A malformed line fails with `invalid_suite`
|
|
130
|
+
* carrying the 1-based line number in both the message and the path; a fixture
|
|
131
|
+
* larger than `limits.fixtureLength` is rejected before any of it is parsed.
|
|
132
|
+
* The first non-blank line after `limits.cases` cases fails with
|
|
133
|
+
* `invalid_suite` at `cases` before it or any later line is parsed or decoded.
|
|
134
|
+
*
|
|
135
|
+
* @category constructors
|
|
136
|
+
* @since 0.1.0
|
|
137
|
+
*/
|
|
138
|
+
export declare const fromJsonLines: (text: string, options: JsonLinesOptions) => Effect.Effect<Suite, EvalError>;
|
|
139
|
+
//# sourceMappingURL=Suite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Suite.d.ts","sourceRoot":"","sources":["../../src/Suite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,KAAK,aAAa,MAAM,yBAAyB,CAAA;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAG1C;;;;;;;;;GASG;AACH,MAAM,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAA;AAE3C;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CACxC;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,CAAA;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAAA;IACtD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACrC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACrC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,MAAM;;;IAGjB,wDAAwD;;CAEhD,CAAA;AAkJV;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,IAAI,GAAI,SAAS,WAAW,KAAG,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAqBtE,CAAA;AAEJ;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAAA;IACtD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACrC;AAQD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,SAAS,gBAAgB,KAAG,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAsClG,CAAA"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed evaluation suite declarations.
|
|
3
|
+
*
|
|
4
|
+
* A suite is the fixed input half of an evaluation: named cases, the scorer
|
|
5
|
+
* bindings that grade them, and the concurrency the runner is allowed. It is
|
|
6
|
+
* validated once and then never changes, because the run that produced a
|
|
7
|
+
* committed baseline has to be reproducible from the suite value that was
|
|
8
|
+
* validated.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.1.0
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Schema from "effect/Schema";
|
|
14
|
+
import { EvalError } from "./EvalError.js";
|
|
15
|
+
import { controlCharacter } from "./internal/controlCharacters.js";
|
|
16
|
+
/**
|
|
17
|
+
* The declared ceilings a suite is validated against.
|
|
18
|
+
*
|
|
19
|
+
* They exist so a mistake fails at construction with a sentence instead of
|
|
20
|
+
* exhausting memory in a runner: `concurrency` bounds the fibers a run may hold
|
|
21
|
+
* open, `cases` bounds one suite, and `fixtureLength` bounds one JSON Lines
|
|
22
|
+
* fixture.
|
|
23
|
+
*
|
|
24
|
+
* @category models
|
|
25
|
+
* @since 0.1.0
|
|
26
|
+
*/
|
|
27
|
+
export const limits = {
|
|
28
|
+
concurrency: 1024,
|
|
29
|
+
cases: 10_000,
|
|
30
|
+
/** Longest JSON Lines fixture, in UTF-16 code units. */
|
|
31
|
+
fixtureLength: 8 * 1024 * 1024
|
|
32
|
+
};
|
|
33
|
+
const invalid = (message, path, cause) => new EvalError({ code: "invalid_suite", message, path, ...(cause === undefined ? {} : { cause }) });
|
|
34
|
+
const cloneMessage = "Suite data must be structured-cloneable so the suite cannot change after it is validated";
|
|
35
|
+
// Validate before structuredClone can erase a prototype or invoke a getter.
|
|
36
|
+
// Only records and arrays can be protected completely with Object.freeze.
|
|
37
|
+
const validateData = (value, path, seen = new WeakSet()) => {
|
|
38
|
+
if (typeof value === "function" || typeof value === "symbol")
|
|
39
|
+
throw invalid(cloneMessage, path);
|
|
40
|
+
if (value === null || typeof value !== "object")
|
|
41
|
+
return;
|
|
42
|
+
if (seen.has(value))
|
|
43
|
+
return;
|
|
44
|
+
seen.add(value);
|
|
45
|
+
const array = Array.isArray(value);
|
|
46
|
+
const prototype = Object.getPrototypeOf(value);
|
|
47
|
+
if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) {
|
|
48
|
+
throw invalid("Suite data must contain only plain objects, arrays, and structured-cloneable primitives", path);
|
|
49
|
+
}
|
|
50
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
51
|
+
if (array && key === "length")
|
|
52
|
+
continue;
|
|
53
|
+
const childPath = typeof key === "string" && array && /^(0|[1-9]\d*)$/.test(key)
|
|
54
|
+
? `${path}[${key}]`
|
|
55
|
+
: `${path}.${String(key)}`;
|
|
56
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
57
|
+
if (typeof key === "symbol" || !descriptor.enumerable || !("value" in descriptor)) {
|
|
58
|
+
throw invalid("Suite data properties must be enumerable string-keyed data fields", childPath);
|
|
59
|
+
}
|
|
60
|
+
validateData(descriptor.value, childPath, seen);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const freezeData = (value, seen = new WeakSet()) => {
|
|
64
|
+
if (value === null || typeof value !== "object" || seen.has(value))
|
|
65
|
+
return value;
|
|
66
|
+
seen.add(value);
|
|
67
|
+
for (const child of Object.values(value))
|
|
68
|
+
freezeData(child, seen);
|
|
69
|
+
return Object.freeze(value);
|
|
70
|
+
};
|
|
71
|
+
const clone = (value, path) => Effect.try({
|
|
72
|
+
try: () => {
|
|
73
|
+
validateData(value, path);
|
|
74
|
+
return freezeData(structuredClone(value));
|
|
75
|
+
},
|
|
76
|
+
catch: (cause) => cause instanceof EvalError ? cause : invalid(cloneMessage, path, cause)
|
|
77
|
+
});
|
|
78
|
+
const validate = (options) => {
|
|
79
|
+
if (options.name.trim().length === 0)
|
|
80
|
+
return invalid("Suite name must not be empty", "name");
|
|
81
|
+
const nameControl = controlCharacter(options.name);
|
|
82
|
+
if (nameControl !== undefined) {
|
|
83
|
+
return invalid(`Suite name must not contain the control character ${nameControl}`, "name");
|
|
84
|
+
}
|
|
85
|
+
if (!Number.isSafeInteger(options.trials) || options.trials < 1 || options.trials > 1000) {
|
|
86
|
+
return invalid("Suite trials must be an integer from 1 to 1000", "trials");
|
|
87
|
+
}
|
|
88
|
+
if (options.cases.length === 0)
|
|
89
|
+
return invalid("Suite must contain at least one case", "cases");
|
|
90
|
+
if (options.cases.length > limits.cases) {
|
|
91
|
+
return invalid(`Suite must contain at most ${limits.cases} cases, got ${options.cases.length}`, "cases");
|
|
92
|
+
}
|
|
93
|
+
if (!Number.isSafeInteger(options.concurrency) || options.concurrency < 1) {
|
|
94
|
+
return invalid(`Suite concurrency must be a positive safe integer, got ${String(options.concurrency)}`, "concurrency");
|
|
95
|
+
}
|
|
96
|
+
if (options.concurrency > limits.concurrency) {
|
|
97
|
+
return invalid(`Suite concurrency must be at most ${limits.concurrency}, got ${options.concurrency}`, "concurrency");
|
|
98
|
+
}
|
|
99
|
+
const names = new Set();
|
|
100
|
+
for (const [index, suiteCase] of options.cases.entries()) {
|
|
101
|
+
if (suiteCase.name.trim().length === 0) {
|
|
102
|
+
return invalid("Suite case name must not be empty", `cases[${index}].name`);
|
|
103
|
+
}
|
|
104
|
+
const caseControl = controlCharacter(suiteCase.name);
|
|
105
|
+
if (caseControl !== undefined) {
|
|
106
|
+
return invalid(`Suite case name must not contain the control character ${caseControl}`, `cases[${index}].name`);
|
|
107
|
+
}
|
|
108
|
+
if (names.has(suiteCase.name)) {
|
|
109
|
+
return invalid(`Duplicate suite case: ${suiteCase.name}`, `cases[${index}].name`);
|
|
110
|
+
}
|
|
111
|
+
names.add(suiteCase.name);
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
};
|
|
115
|
+
// Every field is read exactly once, into these two shapes, before anything is
|
|
116
|
+
// validated or copied. Reading a field twice let a getter return the name that
|
|
117
|
+
// passed validation and then a different one into the suite, which is the same
|
|
118
|
+
// hole a baseline record had.
|
|
119
|
+
const readCase = (suiteCase) => ({
|
|
120
|
+
name: suiteCase.name,
|
|
121
|
+
input: suiteCase.input,
|
|
122
|
+
expected: suiteCase.expected
|
|
123
|
+
});
|
|
124
|
+
const readBinding = (binding) => {
|
|
125
|
+
const read = {
|
|
126
|
+
scorer: binding.scorer,
|
|
127
|
+
appliesTo: binding.appliesTo,
|
|
128
|
+
sampling: binding.sampling
|
|
129
|
+
};
|
|
130
|
+
const groundTruth = binding.groundTruth;
|
|
131
|
+
if (groundTruth !== undefined)
|
|
132
|
+
read.groundTruth = groundTruth;
|
|
133
|
+
const context = binding.context;
|
|
134
|
+
if (context !== undefined)
|
|
135
|
+
read.context = context;
|
|
136
|
+
return read;
|
|
137
|
+
};
|
|
138
|
+
const copyCase = (suiteCase, index) => Effect.gen(function* () {
|
|
139
|
+
const input = yield* clone(suiteCase.input, `cases[${index}].input`);
|
|
140
|
+
if (suiteCase.expected === undefined)
|
|
141
|
+
return Object.freeze({ name: suiteCase.name, input });
|
|
142
|
+
const expected = yield* clone(suiteCase.expected, `cases[${index}].expected`);
|
|
143
|
+
return Object.freeze({ name: suiteCase.name, input, expected });
|
|
144
|
+
});
|
|
145
|
+
const copyBinding = (binding, index) => Effect.gen(function* () {
|
|
146
|
+
// `scorer` and `appliesTo` are executable identities matched by reference,
|
|
147
|
+
// so they are carried over unchanged; sampling, ground truth, and context
|
|
148
|
+
// are inert data that must not remain caller-owned.
|
|
149
|
+
const sampling = binding.sampling === undefined
|
|
150
|
+
? undefined
|
|
151
|
+
: yield* clone(binding.sampling, `bindings[${index}].sampling`);
|
|
152
|
+
const copied = {
|
|
153
|
+
scorer: binding.scorer,
|
|
154
|
+
appliesTo: binding.appliesTo,
|
|
155
|
+
sampling: sampling
|
|
156
|
+
};
|
|
157
|
+
if (binding.groundTruth !== undefined) {
|
|
158
|
+
copied.groundTruth = yield* clone(binding.groundTruth, `bindings[${index}].groundTruth`);
|
|
159
|
+
}
|
|
160
|
+
if (binding.context !== undefined) {
|
|
161
|
+
copied.context = yield* clone(binding.context, `bindings[${index}].context`);
|
|
162
|
+
}
|
|
163
|
+
return Object.freeze(copied);
|
|
164
|
+
});
|
|
165
|
+
/**
|
|
166
|
+
* Builds and validates a fixed suite.
|
|
167
|
+
*
|
|
168
|
+
* When the effect runs, it reads every option, case field, and binding field
|
|
169
|
+
* exactly once, then validates and copies only what it read. Validating one
|
|
170
|
+
* value and copying another let a getter hand the suite something validation
|
|
171
|
+
* never saw. Every case and binding is then copied, including a binding's
|
|
172
|
+
* sampling policy, so the suite is a snapshot the caller can no longer reach:
|
|
173
|
+
* mutating an array, case input, or ratio policy afterwards leaves the
|
|
174
|
+
* validated suite unchanged. Data is checked before `structuredClone` and
|
|
175
|
+
* deeply frozen afterwards. Only plain objects, arrays, and cloneable
|
|
176
|
+
* primitives are admitted, with enumerable string-keyed data properties.
|
|
177
|
+
* Functions, class instances, mutable built-ins, and accessors fail with
|
|
178
|
+
* `invalid_suite` naming the offending path. Cycles and shared references are
|
|
179
|
+
* supported; null-prototype records become ordinary objects in the clone.
|
|
180
|
+
*
|
|
181
|
+
* Fails with `invalid_suite` for an empty or control-character name, no cases,
|
|
182
|
+
* more than `limits.cases` cases, a duplicate case name, or a concurrency that
|
|
183
|
+
* is not a safe integer in `[1, limits.concurrency]`.
|
|
184
|
+
*
|
|
185
|
+
* @category constructors
|
|
186
|
+
* @since 0.1.0
|
|
187
|
+
*/
|
|
188
|
+
export const make = (options) => Effect.suspend(() => {
|
|
189
|
+
const name = options.name;
|
|
190
|
+
const concurrency = options.concurrency;
|
|
191
|
+
const cases = [...options.cases].map(readCase);
|
|
192
|
+
const bindings = [...(options.bindings ?? [])].map(readBinding);
|
|
193
|
+
const trials = options.trials ?? 1;
|
|
194
|
+
const snapshot = { name, concurrency, cases, bindings, trials };
|
|
195
|
+
const error = validate(snapshot);
|
|
196
|
+
if (error !== undefined)
|
|
197
|
+
return Effect.fail(error);
|
|
198
|
+
return Effect.gen(function* () {
|
|
199
|
+
const copiedCases = yield* Effect.forEach(cases, copyCase);
|
|
200
|
+
const copiedBindings = yield* Effect.forEach(bindings, copyBinding);
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
name,
|
|
203
|
+
cases: Object.freeze(copiedCases),
|
|
204
|
+
bindings: Object.freeze(copiedBindings),
|
|
205
|
+
concurrency,
|
|
206
|
+
trials
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
const jsonCase = Schema.Struct({
|
|
211
|
+
name: Schema.String,
|
|
212
|
+
input: Schema.Unknown,
|
|
213
|
+
expected: Schema.optional(Schema.Unknown)
|
|
214
|
+
});
|
|
215
|
+
/**
|
|
216
|
+
* Loads the `{ name, input, expected? }` JSON Lines fixture format.
|
|
217
|
+
*
|
|
218
|
+
* Blank lines are skipped, a leading byte-order mark is stripped, and both LF
|
|
219
|
+
* and CRLF terminate a line. A malformed line fails with `invalid_suite`
|
|
220
|
+
* carrying the 1-based line number in both the message and the path; a fixture
|
|
221
|
+
* larger than `limits.fixtureLength` is rejected before any of it is parsed.
|
|
222
|
+
* The first non-blank line after `limits.cases` cases fails with
|
|
223
|
+
* `invalid_suite` at `cases` before it or any later line is parsed or decoded.
|
|
224
|
+
*
|
|
225
|
+
* @category constructors
|
|
226
|
+
* @since 0.1.0
|
|
227
|
+
*/
|
|
228
|
+
export const fromJsonLines = (text, options) => Effect.gen(function* () {
|
|
229
|
+
if (text.length > limits.fixtureLength) {
|
|
230
|
+
return yield* Effect.fail(invalid(`JSON Lines fixture must be at most ${limits.fixtureLength} characters, got ${text.length}`, "text"));
|
|
231
|
+
}
|
|
232
|
+
const cases = [];
|
|
233
|
+
const body = text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
234
|
+
for (const [index, line] of body.split(/\r?\n/).entries()) {
|
|
235
|
+
const trimmed = line.trim();
|
|
236
|
+
if (trimmed.length === 0)
|
|
237
|
+
continue;
|
|
238
|
+
// Stop at the ceiling before parsing or decoding the excess line: a
|
|
239
|
+
// fixture under the length limit can still hold hundreds of thousands
|
|
240
|
+
// of tiny lines, and reporting the count only in make() would parse and
|
|
241
|
+
// materialize every one of them first.
|
|
242
|
+
if (cases.length === limits.cases) {
|
|
243
|
+
return yield* Effect.fail(invalid(`Suite must contain at most ${limits.cases} cases, got ${cases.length + 1}`, "cases"));
|
|
244
|
+
}
|
|
245
|
+
const decoded = yield* Effect.try({
|
|
246
|
+
try: () => JSON.parse(trimmed),
|
|
247
|
+
catch: (cause) => invalid(`Invalid JSON on line ${index + 1}`, `line[${index + 1}]`, cause)
|
|
248
|
+
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(jsonCase)), Effect.mapError((cause) => cause instanceof EvalError
|
|
249
|
+
? cause
|
|
250
|
+
: invalid(`Invalid suite case on line ${index + 1}`, `line[${index + 1}]`, cause)));
|
|
251
|
+
cases.push(decoded);
|
|
252
|
+
}
|
|
253
|
+
return yield* make({ ...options, cases });
|
|
254
|
+
});
|
|
255
|
+
//# sourceMappingURL=Suite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Suite.js","sourceRoot":"","sources":["../../src/Suite.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fixed evaluation suite declarations.\n *\n * A suite is the fixed input half of an evaluation: named cases, the scorer\n * bindings that grade them, and the concurrency the runner is allowed. It is\n * validated once and then never changes, because the run that produced a\n * committed baseline has to be reproducible from the suite value that was\n * validated.\n *\n * @since 0.1.0\n */\n\nimport type * as ScorerBinding from \"@smthrs/scorers/Binding\"\nimport * as Effect from \"effect/Effect\"\nimport * as Schema from \"effect/Schema\"\nimport { EvalError } from \"./EvalError.ts\"\nimport { controlCharacter } from \"./internal/controlCharacters.ts\"\n\n/**\n * A scorer binding accepted from `@smthrs/scorers`.\n *\n * The binding's `appliesTo` flow is matched against an execution's `target` by\n * reference identity, so a binding only ever grades the exact flow value it was\n * declared against.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Binding = ScorerBinding.Binding\n\n/**\n * One immutable fixed-suite case.\n *\n * `input` is handed to the executor and `expected` is offered to a bound scorer\n * as ground truth. A declared `expected`, including `null`, wins over the\n * binding's `groundTruth`; only an absent one defers to the binding. Both are\n * snapshots taken by {@link make}: mutating the object a caller passed in never\n * changes the suite.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Case {\n readonly name: string\n readonly input: unknown\n readonly expected?: unknown | undefined\n}\n\n/**\n * Options for constructing a suite.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface MakeOptions {\n readonly name: string\n readonly cases: ReadonlyArray<Case>\n readonly bindings?: ReadonlyArray<Binding> | undefined\n readonly concurrency: number\n readonly trials?: number | undefined\n}\n\n/**\n * A validated, named collection of fixed cases and scorer bindings.\n *\n * The suite, cases, bindings, and their inert data are deeply frozen snapshots.\n * Executable scorer and target identities remain unchanged.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Suite {\n readonly name: string\n readonly cases: ReadonlyArray<Case>\n readonly bindings: ReadonlyArray<Binding>\n readonly concurrency: number\n readonly trials?: number | undefined\n}\n\n/**\n * The declared ceilings a suite is validated against.\n *\n * They exist so a mistake fails at construction with a sentence instead of\n * exhausting memory in a runner: `concurrency` bounds the fibers a run may hold\n * open, `cases` bounds one suite, and `fixtureLength` bounds one JSON Lines\n * fixture.\n *\n * @category models\n * @since 0.1.0\n */\nexport const limits = {\n concurrency: 1024,\n cases: 10_000,\n /** Longest JSON Lines fixture, in UTF-16 code units. */\n fixtureLength: 8 * 1024 * 1024\n} as const\n\nconst invalid = (message: string, path: string, cause?: unknown): EvalError =>\n new EvalError({ code: \"invalid_suite\", message, path, ...(cause === undefined ? {} : { cause }) })\n\nconst cloneMessage = \"Suite data must be structured-cloneable so the suite cannot change after it is validated\"\n\n// Validate before structuredClone can erase a prototype or invoke a getter.\n// Only records and arrays can be protected completely with Object.freeze.\nconst validateData = (value: unknown, path: string, seen = new WeakSet<object>()): void => {\n if (typeof value === \"function\" || typeof value === \"symbol\") throw invalid(cloneMessage, path)\n if (value === null || typeof value !== \"object\") return\n if (seen.has(value)) return\n seen.add(value)\n const array = Array.isArray(value)\n const prototype = Object.getPrototypeOf(value)\n if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) {\n throw invalid(\"Suite data must contain only plain objects, arrays, and structured-cloneable primitives\", path)\n }\n for (const key of Reflect.ownKeys(value)) {\n if (array && key === \"length\") continue\n const childPath = typeof key === \"string\" && array && /^(0|[1-9]\\d*)$/.test(key)\n ? `${path}[${key}]`\n : `${path}.${String(key)}`\n const descriptor = Object.getOwnPropertyDescriptor(value, key)!\n if (typeof key === \"symbol\" || !descriptor.enumerable || !(\"value\" in descriptor)) {\n throw invalid(\"Suite data properties must be enumerable string-keyed data fields\", childPath)\n }\n validateData(descriptor.value, childPath, seen)\n }\n}\n\nconst freezeData = (value: unknown, seen = new WeakSet<object>()): unknown => {\n if (value === null || typeof value !== \"object\" || seen.has(value)) return value\n seen.add(value)\n for (const child of Object.values(value)) freezeData(child, seen)\n return Object.freeze(value)\n}\n\nconst clone = (value: unknown, path: string): Effect.Effect<unknown, EvalError> =>\n Effect.try({\n try: () => {\n validateData(value, path)\n return freezeData(structuredClone(value))\n },\n catch: (cause) => cause instanceof EvalError ? cause : invalid(cloneMessage, path, cause)\n })\n\nconst validate = (options: MakeOptions & { readonly trials: number }): EvalError | undefined => {\n if (options.name.trim().length === 0) return invalid(\"Suite name must not be empty\", \"name\")\n const nameControl = controlCharacter(options.name)\n if (nameControl !== undefined) {\n return invalid(`Suite name must not contain the control character ${nameControl}`, \"name\")\n }\n if (!Number.isSafeInteger(options.trials) || options.trials < 1 || options.trials > 1000) {\n return invalid(\"Suite trials must be an integer from 1 to 1000\", \"trials\")\n }\n if (options.cases.length === 0) return invalid(\"Suite must contain at least one case\", \"cases\")\n if (options.cases.length > limits.cases) {\n return invalid(`Suite must contain at most ${limits.cases} cases, got ${options.cases.length}`, \"cases\")\n }\n if (!Number.isSafeInteger(options.concurrency) || options.concurrency < 1) {\n return invalid(\n `Suite concurrency must be a positive safe integer, got ${String(options.concurrency)}`,\n \"concurrency\"\n )\n }\n if (options.concurrency > limits.concurrency) {\n return invalid(\n `Suite concurrency must be at most ${limits.concurrency}, got ${options.concurrency}`,\n \"concurrency\"\n )\n }\n const names = new Set<string>()\n for (const [index, suiteCase] of options.cases.entries()) {\n if (suiteCase.name.trim().length === 0) {\n return invalid(\"Suite case name must not be empty\", `cases[${index}].name`)\n }\n const caseControl = controlCharacter(suiteCase.name)\n if (caseControl !== undefined) {\n return invalid(\n `Suite case name must not contain the control character ${caseControl}`,\n `cases[${index}].name`\n )\n }\n if (names.has(suiteCase.name)) {\n return invalid(`Duplicate suite case: ${suiteCase.name}`, `cases[${index}].name`)\n }\n names.add(suiteCase.name)\n }\n return undefined\n}\n\n// Every field is read exactly once, into these two shapes, before anything is\n// validated or copied. Reading a field twice let a getter return the name that\n// passed validation and then a different one into the suite, which is the same\n// hole a baseline record had.\nconst readCase = (suiteCase: Case): Case => ({\n name: suiteCase.name,\n input: suiteCase.input,\n expected: suiteCase.expected\n})\n\nconst readBinding = (binding: Binding): Binding => {\n const read: { -readonly [K in keyof Binding]: Binding[K] } = {\n scorer: binding.scorer,\n appliesTo: binding.appliesTo,\n sampling: binding.sampling\n }\n const groundTruth = binding.groundTruth\n if (groundTruth !== undefined) read.groundTruth = groundTruth\n const context = binding.context\n if (context !== undefined) read.context = context\n return read\n}\n\nconst copyCase = (suiteCase: Case, index: number): Effect.Effect<Case, EvalError> =>\n Effect.gen(function*() {\n const input = yield* clone(suiteCase.input, `cases[${index}].input`)\n if (suiteCase.expected === undefined) return Object.freeze({ name: suiteCase.name, input })\n const expected = yield* clone(suiteCase.expected, `cases[${index}].expected`)\n return Object.freeze({ name: suiteCase.name, input, expected })\n })\n\nconst copyBinding = (binding: Binding, index: number): Effect.Effect<Binding, EvalError> =>\n Effect.gen(function*() {\n // `scorer` and `appliesTo` are executable identities matched by reference,\n // so they are carried over unchanged; sampling, ground truth, and context\n // are inert data that must not remain caller-owned.\n const sampling = binding.sampling === undefined\n ? undefined\n : yield* clone(binding.sampling, `bindings[${index}].sampling`)\n const copied: { -readonly [K in keyof Binding]: Binding[K] } = {\n scorer: binding.scorer,\n appliesTo: binding.appliesTo,\n sampling: sampling as Binding[\"sampling\"]\n }\n if (binding.groundTruth !== undefined) {\n copied.groundTruth = yield* clone(binding.groundTruth, `bindings[${index}].groundTruth`)\n }\n if (binding.context !== undefined) {\n copied.context = yield* clone(binding.context, `bindings[${index}].context`)\n }\n return Object.freeze(copied)\n })\n\n/**\n * Builds and validates a fixed suite.\n *\n * When the effect runs, it reads every option, case field, and binding field\n * exactly once, then validates and copies only what it read. Validating one\n * value and copying another let a getter hand the suite something validation\n * never saw. Every case and binding is then copied, including a binding's\n * sampling policy, so the suite is a snapshot the caller can no longer reach:\n * mutating an array, case input, or ratio policy afterwards leaves the\n * validated suite unchanged. Data is checked before `structuredClone` and\n * deeply frozen afterwards. Only plain objects, arrays, and cloneable\n * primitives are admitted, with enumerable string-keyed data properties.\n * Functions, class instances, mutable built-ins, and accessors fail with\n * `invalid_suite` naming the offending path. Cycles and shared references are\n * supported; null-prototype records become ordinary objects in the clone.\n *\n * Fails with `invalid_suite` for an empty or control-character name, no cases,\n * more than `limits.cases` cases, a duplicate case name, or a concurrency that\n * is not a safe integer in `[1, limits.concurrency]`.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (options: MakeOptions): Effect.Effect<Suite, EvalError> =>\n Effect.suspend(() => {\n const name = options.name\n const concurrency = options.concurrency\n const cases = [...options.cases].map(readCase)\n const bindings = [...(options.bindings ?? [])].map(readBinding)\n const trials = options.trials ?? 1\n const snapshot = { name, concurrency, cases, bindings, trials }\n const error = validate(snapshot)\n if (error !== undefined) return Effect.fail(error)\n return Effect.gen(function*() {\n const copiedCases = yield* Effect.forEach(cases, copyCase)\n const copiedBindings = yield* Effect.forEach(bindings, copyBinding)\n return Object.freeze({\n name,\n cases: Object.freeze(copiedCases),\n bindings: Object.freeze(copiedBindings),\n concurrency,\n trials\n })\n })\n })\n\n/**\n * Options used when decoding JSON Lines.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface JsonLinesOptions {\n readonly name: string\n readonly bindings?: ReadonlyArray<Binding> | undefined\n readonly concurrency: number\n readonly trials?: number | undefined\n}\n\nconst jsonCase = Schema.Struct({\n name: Schema.String,\n input: Schema.Unknown,\n expected: Schema.optional(Schema.Unknown)\n})\n\n/**\n * Loads the `{ name, input, expected? }` JSON Lines fixture format.\n *\n * Blank lines are skipped, a leading byte-order mark is stripped, and both LF\n * and CRLF terminate a line. A malformed line fails with `invalid_suite`\n * carrying the 1-based line number in both the message and the path; a fixture\n * larger than `limits.fixtureLength` is rejected before any of it is parsed.\n * The first non-blank line after `limits.cases` cases fails with\n * `invalid_suite` at `cases` before it or any later line is parsed or decoded.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const fromJsonLines = (text: string, options: JsonLinesOptions): Effect.Effect<Suite, EvalError> =>\n Effect.gen(function*() {\n if (text.length > limits.fixtureLength) {\n return yield* Effect.fail(\n invalid(\n `JSON Lines fixture must be at most ${limits.fixtureLength} characters, got ${text.length}`,\n \"text\"\n )\n )\n }\n const cases: Array<Case> = []\n const body = text.startsWith(\"\\uFEFF\") ? text.slice(1) : text\n for (const [index, line] of body.split(/\\r?\\n/).entries()) {\n const trimmed = line.trim()\n if (trimmed.length === 0) continue\n // Stop at the ceiling before parsing or decoding the excess line: a\n // fixture under the length limit can still hold hundreds of thousands\n // of tiny lines, and reporting the count only in make() would parse and\n // materialize every one of them first.\n if (cases.length === limits.cases) {\n return yield* Effect.fail(\n invalid(`Suite must contain at most ${limits.cases} cases, got ${cases.length + 1}`, \"cases\")\n )\n }\n const decoded = yield* Effect.try({\n try: () => JSON.parse(trimmed) as unknown,\n catch: (cause) => invalid(`Invalid JSON on line ${index + 1}`, `line[${index + 1}]`, cause)\n }).pipe(\n Effect.flatMap(Schema.decodeUnknownEffect(jsonCase)),\n Effect.mapError((cause) =>\n cause instanceof EvalError\n ? cause\n : invalid(`Invalid suite case on line ${index + 1}`, `line[${index + 1}]`, cause)\n )\n )\n cases.push(decoded)\n }\n return yield* make({ ...options, cases })\n })\n"]}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repeated-run statistics for live model evaluations.
|
|
3
|
+
*
|
|
4
|
+
* A live case runs `n` times and passes `c` of them. {@link passAtK} is the
|
|
5
|
+
* chance that at least one of `k` runs passes, the capability a best-of-k
|
|
6
|
+
* caller sees; {@link passHatK} is the chance that all `k` pass, the
|
|
7
|
+
* consistency a user sees on every attempt. Both are exact for sampling `k` of
|
|
8
|
+
* the `n` recorded runs without replacement.
|
|
9
|
+
*
|
|
10
|
+
* Every function is pure and throws an `EvalError` with code `invalid_trials`
|
|
11
|
+
* for counts outside the documented bounds.
|
|
12
|
+
*
|
|
13
|
+
* @since 0.1.0
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Repeated-run statistics for one case.
|
|
17
|
+
*
|
|
18
|
+
* `rate` equals `passAt1`; `stderr` is the standard error of the pass rate,
|
|
19
|
+
* `sqrt(rate * (1 - rate) / n)`, and 0 for a single run.
|
|
20
|
+
*
|
|
21
|
+
* @since 0.1.0
|
|
22
|
+
* @category models
|
|
23
|
+
*/
|
|
24
|
+
export interface Summary {
|
|
25
|
+
readonly n: number;
|
|
26
|
+
readonly passes: number;
|
|
27
|
+
readonly rate: number;
|
|
28
|
+
readonly passAt1: number;
|
|
29
|
+
readonly passAtK: number;
|
|
30
|
+
readonly passHatK: number;
|
|
31
|
+
readonly stderr: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Repeated-run statistics across cases.
|
|
35
|
+
*
|
|
36
|
+
* `passAt1`, `passAtK`, and `passHatK` are means over cases, and `allPass` is
|
|
37
|
+
* the fraction of cases whose every run passed.
|
|
38
|
+
*
|
|
39
|
+
* @since 0.1.0
|
|
40
|
+
* @category models
|
|
41
|
+
*/
|
|
42
|
+
export interface Aggregate {
|
|
43
|
+
readonly cases: number;
|
|
44
|
+
readonly passAt1: number;
|
|
45
|
+
readonly passAtK: number;
|
|
46
|
+
readonly passHatK: number;
|
|
47
|
+
readonly allPass: number;
|
|
48
|
+
readonly perCase: Readonly<Record<string, Summary>>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The unbiased pass@k estimator, `1 - C(n - c, k) / C(n, k)` (Chen et al.,
|
|
52
|
+
* 2021): the chance that at least one of `k` runs drawn from `n` passes. It is
|
|
53
|
+
* computed as a product, so large `n` neither overflows nor loses precision.
|
|
54
|
+
*
|
|
55
|
+
* Throws `invalid_trials` unless `n`, `c`, and `k` are integers with
|
|
56
|
+
* `0 <= c <= n` and `1 <= k <= n`.
|
|
57
|
+
*
|
|
58
|
+
* @since 0.1.0
|
|
59
|
+
* @category statistics
|
|
60
|
+
*/
|
|
61
|
+
export declare const passAtK: (n: number, c: number, k: number) => number;
|
|
62
|
+
/**
|
|
63
|
+
* pass^k, `C(c, k) / C(n, k)`: the chance that all `k` runs drawn from `n`
|
|
64
|
+
* without replacement pass, the consistency measure of tau-bench.
|
|
65
|
+
*
|
|
66
|
+
* Throws `invalid_trials` under the same bounds as {@link passAtK}.
|
|
67
|
+
*
|
|
68
|
+
* @since 0.1.0
|
|
69
|
+
* @category statistics
|
|
70
|
+
*/
|
|
71
|
+
export declare const passHatK: (n: number, c: number, k: number) => number;
|
|
72
|
+
/**
|
|
73
|
+
* Summarizes one case's run outcomes at `k`.
|
|
74
|
+
*
|
|
75
|
+
* Throws `invalid_trials` for no outcomes or a `k` outside `1..n`.
|
|
76
|
+
*
|
|
77
|
+
* @since 0.1.0
|
|
78
|
+
* @category statistics
|
|
79
|
+
*/
|
|
80
|
+
export declare const summarize: (outcomes: ReadonlyArray<boolean>, k: number) => Summary;
|
|
81
|
+
/**
|
|
82
|
+
* Summarizes every case at `k` and averages over cases.
|
|
83
|
+
*
|
|
84
|
+
* A case with fewer than `k` runs is summarized at `k = n`, its own run
|
|
85
|
+
* count, so a short case still contributes rather than failing the whole
|
|
86
|
+
* aggregate. No cases aggregate to zeros. Throws `invalid_trials` for a case
|
|
87
|
+
* with no runs or a `k` that is not a positive integer.
|
|
88
|
+
*
|
|
89
|
+
* @since 0.1.0
|
|
90
|
+
* @category statistics
|
|
91
|
+
*/
|
|
92
|
+
export declare const aggregate: (cases: Readonly<Record<string, ReadonlyArray<boolean>>>, k: number) => Aggregate;
|
|
93
|
+
//# sourceMappingURL=Trials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Trials.d.ts","sourceRoot":"","sources":["../../src/Trials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;;;;;;GAQG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACpD;AAYD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,OAAO,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,MAMzD,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,MAK1D,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,GAAI,UAAU,aAAa,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,KAAG,OAcvE,CAAA;AAKD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,KAAG,SAgB9F,CAAA"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repeated-run statistics for live model evaluations.
|
|
3
|
+
*
|
|
4
|
+
* A live case runs `n` times and passes `c` of them. {@link passAtK} is the
|
|
5
|
+
* chance that at least one of `k` runs passes, the capability a best-of-k
|
|
6
|
+
* caller sees; {@link passHatK} is the chance that all `k` pass, the
|
|
7
|
+
* consistency a user sees on every attempt. Both are exact for sampling `k` of
|
|
8
|
+
* the `n` recorded runs without replacement.
|
|
9
|
+
*
|
|
10
|
+
* Every function is pure and throws an `EvalError` with code `invalid_trials`
|
|
11
|
+
* for counts outside the documented bounds.
|
|
12
|
+
*
|
|
13
|
+
* @since 0.1.0
|
|
14
|
+
*/
|
|
15
|
+
import { EvalError } from "./EvalError.js";
|
|
16
|
+
const invalid = (message, path) => new EvalError({ code: "invalid_trials", message, path });
|
|
17
|
+
const validate = (n, c, k) => {
|
|
18
|
+
if (!Number.isInteger(n) || n < 1)
|
|
19
|
+
throw invalid(`Run count must be a positive integer, received ${n}`, "n");
|
|
20
|
+
if (!Number.isInteger(c) || c < 0 || c > n) {
|
|
21
|
+
throw invalid(`Pass count must be an integer from 0 to ${n}, received ${c}`, "c");
|
|
22
|
+
}
|
|
23
|
+
if (!Number.isInteger(k) || k < 1 || k > n)
|
|
24
|
+
throw invalid(`k must be an integer from 1 to ${n}, received ${k}`, "k");
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The unbiased pass@k estimator, `1 - C(n - c, k) / C(n, k)` (Chen et al.,
|
|
28
|
+
* 2021): the chance that at least one of `k` runs drawn from `n` passes. It is
|
|
29
|
+
* computed as a product, so large `n` neither overflows nor loses precision.
|
|
30
|
+
*
|
|
31
|
+
* Throws `invalid_trials` unless `n`, `c`, and `k` are integers with
|
|
32
|
+
* `0 <= c <= n` and `1 <= k <= n`.
|
|
33
|
+
*
|
|
34
|
+
* @since 0.1.0
|
|
35
|
+
* @category statistics
|
|
36
|
+
*/
|
|
37
|
+
export const passAtK = (n, c, k) => {
|
|
38
|
+
validate(n, c, k);
|
|
39
|
+
if (n - c < k)
|
|
40
|
+
return 1;
|
|
41
|
+
let fail = 1;
|
|
42
|
+
for (let i = n - c + 1; i <= n; i++)
|
|
43
|
+
fail *= 1 - k / i;
|
|
44
|
+
return 1 - fail;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* pass^k, `C(c, k) / C(n, k)`: the chance that all `k` runs drawn from `n`
|
|
48
|
+
* without replacement pass, the consistency measure of tau-bench.
|
|
49
|
+
*
|
|
50
|
+
* Throws `invalid_trials` under the same bounds as {@link passAtK}.
|
|
51
|
+
*
|
|
52
|
+
* @since 0.1.0
|
|
53
|
+
* @category statistics
|
|
54
|
+
*/
|
|
55
|
+
export const passHatK = (n, c, k) => {
|
|
56
|
+
validate(n, c, k);
|
|
57
|
+
let all = 1;
|
|
58
|
+
for (let i = 0; i < k; i++)
|
|
59
|
+
all *= (c - i) / (n - i);
|
|
60
|
+
return Math.max(0, all);
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Summarizes one case's run outcomes at `k`.
|
|
64
|
+
*
|
|
65
|
+
* Throws `invalid_trials` for no outcomes or a `k` outside `1..n`.
|
|
66
|
+
*
|
|
67
|
+
* @since 0.1.0
|
|
68
|
+
* @category statistics
|
|
69
|
+
*/
|
|
70
|
+
export const summarize = (outcomes, k) => {
|
|
71
|
+
const n = outcomes.length;
|
|
72
|
+
const passes = outcomes.filter(Boolean).length;
|
|
73
|
+
validate(n, passes, k);
|
|
74
|
+
const rate = passes / n;
|
|
75
|
+
return {
|
|
76
|
+
n,
|
|
77
|
+
passes,
|
|
78
|
+
rate,
|
|
79
|
+
passAt1: rate,
|
|
80
|
+
passAtK: passAtK(n, passes, k),
|
|
81
|
+
passHatK: passHatK(n, passes, k),
|
|
82
|
+
stderr: n <= 1 ? 0 : Math.sqrt(rate * (1 - rate) / n)
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const mean = (values) => values.length === 0 ? 0 : values.reduce((total, value) => total + value, 0) / values.length;
|
|
86
|
+
/**
|
|
87
|
+
* Summarizes every case at `k` and averages over cases.
|
|
88
|
+
*
|
|
89
|
+
* A case with fewer than `k` runs is summarized at `k = n`, its own run
|
|
90
|
+
* count, so a short case still contributes rather than failing the whole
|
|
91
|
+
* aggregate. No cases aggregate to zeros. Throws `invalid_trials` for a case
|
|
92
|
+
* with no runs or a `k` that is not a positive integer.
|
|
93
|
+
*
|
|
94
|
+
* @since 0.1.0
|
|
95
|
+
* @category statistics
|
|
96
|
+
*/
|
|
97
|
+
export const aggregate = (cases, k) => {
|
|
98
|
+
if (!Number.isInteger(k) || k < 1)
|
|
99
|
+
throw invalid(`k must be a positive integer, received ${k}`, "k");
|
|
100
|
+
const perCase = {};
|
|
101
|
+
for (const [name, outcomes] of Object.entries(cases)) {
|
|
102
|
+
if (outcomes.length === 0)
|
|
103
|
+
throw invalid(`Case '${name}' has no runs`, `cases['${name}']`);
|
|
104
|
+
perCase[name] = summarize(outcomes, Math.min(k, outcomes.length));
|
|
105
|
+
}
|
|
106
|
+
const summaries = Object.values(perCase);
|
|
107
|
+
return {
|
|
108
|
+
cases: summaries.length,
|
|
109
|
+
passAt1: mean(summaries.map((summary) => summary.passAt1)),
|
|
110
|
+
passAtK: mean(summaries.map((summary) => summary.passAtK)),
|
|
111
|
+
passHatK: mean(summaries.map((summary) => summary.passHatK)),
|
|
112
|
+
allPass: mean(summaries.map((summary) => summary.passes === summary.n ? 1 : 0)),
|
|
113
|
+
perCase
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
//# sourceMappingURL=Trials.js.map
|