@smthrs/evals 0.0.0-stage → 1.0.0-rc.12

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 +66 -2
  4. package/dist/cjs/Baseline.d.ts +120 -0
  5. package/dist/cjs/Baseline.d.ts.map +1 -0
  6. package/dist/cjs/Baseline.js +160 -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 +192 -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 +453 -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 +120 -0
  62. package/dist/esm/Baseline.d.ts.map +1 -0
  63. package/dist/esm/Baseline.js +208 -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 +200 -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 +455 -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 +117 -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 +128 -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 +32 -0
  127. package/docs/quickstart.md +238 -0
  128. package/docs/troubleshooting.md +153 -0
  129. package/package.json +198 -3
  130. package/src/Baseline.ts +266 -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 +321 -0
  135. package/src/Report.ts +268 -0
  136. package/src/Runner.ts +789 -0
  137. package/src/Suite.ts +358 -0
  138. package/src/Trials.ts +155 -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,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,SAiB9F,CAAA"}
@@ -0,0 +1,117 @@
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 entries = Object.entries(cases).map(([name, outcomes]) => {
101
+ if (outcomes.length === 0)
102
+ throw invalid(`Case '${name}' has no runs`, `cases['${name}']`);
103
+ return [name, summarize(outcomes, Math.min(k, outcomes.length))];
104
+ });
105
+ // fromEntries defines own properties, so a case named `__proto__` stays a case.
106
+ const perCase = Object.fromEntries(entries);
107
+ const summaries = Object.values(perCase);
108
+ return {
109
+ cases: summaries.length,
110
+ passAt1: mean(summaries.map((summary) => summary.passAt1)),
111
+ passAtK: mean(summaries.map((summary) => summary.passAtK)),
112
+ passHatK: mean(summaries.map((summary) => summary.passHatK)),
113
+ allPass: mean(summaries.map((summary) => summary.passes === summary.n ? 1 : 0)),
114
+ perCase
115
+ };
116
+ };
117
+ //# sourceMappingURL=Trials.js.map