@rulvar/evals 1.0.0
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/LICENSE +202 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.js +488 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { CompiledWorkflow, Effort, Engine, EvidenceRef, Json, JsonSchema, ModelClaim, ModelKnowledgeStore, ModelRef, ModelSpec, RunOutcome, SchemaSpec, TaskClass, Usage, WireError, Workflow } from "@rulvar/core";
|
|
2
|
+
|
|
3
|
+
//#region src/case.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* One quality-measurement case (docs/09, section 7.1). The shape is the
|
|
6
|
+
* documented interface verbatim; display names derive from the workflow
|
|
7
|
+
* name (the suite runner disambiguates duplicates by ordinal).
|
|
8
|
+
*/
|
|
9
|
+
interface EvalCase {
|
|
10
|
+
workflow: Workflow | CompiledWorkflow;
|
|
11
|
+
args: Json;
|
|
12
|
+
graders: Grader[];
|
|
13
|
+
}
|
|
14
|
+
/** One grader's outcome for one case. */
|
|
15
|
+
interface GraderVerdict {
|
|
16
|
+
/** The grader's display name. */
|
|
17
|
+
grader: string;
|
|
18
|
+
passed: boolean;
|
|
19
|
+
/** 0..1 where the family defines a fraction (rubric criteria met). */
|
|
20
|
+
score?: number;
|
|
21
|
+
/** Family-specific evidence: diffs, per-criterion verdicts, judge output. */
|
|
22
|
+
details?: Json;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A judge invocation specification. The judge runs through the engine as
|
|
26
|
+
* an ordinary journaled, budgeted invocation; model selection is subject
|
|
27
|
+
* to the router quality floors (docs/04, section "Role quality floors"),
|
|
28
|
+
* and @rulvar/evals ships NO default judge model: weak defaults for
|
|
29
|
+
* judging are forbidden, so the model is always explicit.
|
|
30
|
+
*/
|
|
31
|
+
interface JudgeSpec {
|
|
32
|
+
model: ModelSpec;
|
|
33
|
+
prompt: string;
|
|
34
|
+
schema: SchemaSpec;
|
|
35
|
+
}
|
|
36
|
+
/** What a grader sees; judge() is the only channel back into the engine. */
|
|
37
|
+
interface GraderContext {
|
|
38
|
+
/** The target run's structured output (RunOutcome.value). */
|
|
39
|
+
value: Json | undefined;
|
|
40
|
+
/** The full target outcome, for status- and cost-aware graders. */
|
|
41
|
+
outcome: RunOutcome<Json>;
|
|
42
|
+
/**
|
|
43
|
+
* Runs one judge invocation through the engine (journaled, budgeted,
|
|
44
|
+
* VCR-recordable) and returns the judge's structured output. Throws
|
|
45
|
+
* when the judge run itself does not settle ok.
|
|
46
|
+
*/
|
|
47
|
+
judge(spec: JudgeSpec): Promise<Json>;
|
|
48
|
+
}
|
|
49
|
+
interface Grader {
|
|
50
|
+
name: string;
|
|
51
|
+
grade(context: GraderContext): Promise<GraderVerdict> | GraderVerdict;
|
|
52
|
+
}
|
|
53
|
+
/** The measured result of one EvalCase. */
|
|
54
|
+
interface EvalCaseResult {
|
|
55
|
+
/** Workflow name, disambiguated by the suite runner on duplicates. */
|
|
56
|
+
name: string;
|
|
57
|
+
/** The target run's settle status. */
|
|
58
|
+
status: RunOutcome<Json>["status"];
|
|
59
|
+
/** status 'ok' AND every grader passed. */
|
|
60
|
+
passed: boolean;
|
|
61
|
+
verdicts: GraderVerdict[];
|
|
62
|
+
/** Target run cost plus all judge run costs (CostReport.totalUsd sums). */
|
|
63
|
+
costUsd: number;
|
|
64
|
+
/** The judge-run share of costUsd. */
|
|
65
|
+
judgeCostUsd: number;
|
|
66
|
+
/**
|
|
67
|
+
* run:start to run:end of the target run, from event timestamps; no
|
|
68
|
+
* separate measurement channel exists (docs/09, section 7.2).
|
|
69
|
+
*/
|
|
70
|
+
latencyMs: number;
|
|
71
|
+
/** The target run's normalized usage. */
|
|
72
|
+
usage: Usage;
|
|
73
|
+
error?: WireError;
|
|
74
|
+
}
|
|
75
|
+
interface RunEvalCaseOptions {
|
|
76
|
+
/** Display-name override; defaults to the workflow name. */
|
|
77
|
+
name?: string;
|
|
78
|
+
/** Run ceiling for the target run. */
|
|
79
|
+
budgetUsd?: number;
|
|
80
|
+
/** Run ceiling for each judge run. */
|
|
81
|
+
judgeBudgetUsd?: number;
|
|
82
|
+
}
|
|
83
|
+
/** Thrown when a judge run does not settle ok. */
|
|
84
|
+
declare class EvalJudgeError extends Error {
|
|
85
|
+
readonly judgeRun: string;
|
|
86
|
+
readonly status: RunOutcome<Json>["status"];
|
|
87
|
+
constructor(judgeRun: string, status: RunOutcome<Json>["status"], detail?: string);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Runs one EvalCase on the given engine: the target workflow as its own
|
|
91
|
+
* run, pure graders host-side over the outcome, judge graders through the
|
|
92
|
+
* engine via GraderContext.judge. Grader thrown errors are not absorbed:
|
|
93
|
+
* a grader that cannot grade is a defect of the suite, not a failed case.
|
|
94
|
+
*/
|
|
95
|
+
declare function runEvalCase(engine: Engine, evalCase: EvalCase, options?: RunEvalCaseOptions): Promise<EvalCaseResult>;
|
|
96
|
+
/** Aggregate view of a suite run. */
|
|
97
|
+
interface EvalSuiteResult {
|
|
98
|
+
results: EvalCaseResult[];
|
|
99
|
+
/** Fraction of cases with passed true; 0 for an empty suite. */
|
|
100
|
+
passRate: number;
|
|
101
|
+
totalCostUsd: number;
|
|
102
|
+
/** Arithmetic mean over cases; 0 for an empty suite. */
|
|
103
|
+
meanLatencyMs: number;
|
|
104
|
+
}
|
|
105
|
+
interface RunEvalSuiteOptions {
|
|
106
|
+
budgetUsd?: number;
|
|
107
|
+
judgeBudgetUsd?: number;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Runs cases sequentially (deterministic journal and cassette order) and
|
|
111
|
+
* aggregates. Duplicate workflow names get '#<ordinal>' suffixes so every
|
|
112
|
+
* result row and judge journal is unambiguous.
|
|
113
|
+
*/
|
|
114
|
+
declare function runEvalSuite(engine: Engine, cases: EvalCase[], options?: RunEvalSuiteOptions): Promise<EvalSuiteResult>;
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/matrix.d.ts
|
|
117
|
+
/** One configuration under comparison. */
|
|
118
|
+
interface MatrixCell {
|
|
119
|
+
name: string;
|
|
120
|
+
/** A fresh engine per cell run keeps cells isolated. */
|
|
121
|
+
engine: () => Engine | Promise<Engine>;
|
|
122
|
+
}
|
|
123
|
+
interface MatrixCellReport {
|
|
124
|
+
cell: string;
|
|
125
|
+
passRate: number;
|
|
126
|
+
totalCostUsd: number;
|
|
127
|
+
meanLatencyMs: number;
|
|
128
|
+
results: EvalCaseResult[];
|
|
129
|
+
}
|
|
130
|
+
interface EvalMatrixReport {
|
|
131
|
+
cells: MatrixCellReport[];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Runs the same case list against every cell's engine, sequentially and
|
|
135
|
+
* in declaration order (deterministic cassette consumption), and reports
|
|
136
|
+
* per-cell aggregates for side-by-side comparison.
|
|
137
|
+
*/
|
|
138
|
+
declare function runEvalMatrix(cells: MatrixCell[], cases: EvalCase[], options?: RunEvalSuiteOptions): Promise<EvalMatrixReport>;
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/graders/golden.d.ts
|
|
141
|
+
interface GoldenGraderOptions {
|
|
142
|
+
name?: string;
|
|
143
|
+
}
|
|
144
|
+
declare function goldenGrader(expected: Json, options?: GoldenGraderOptions): Grader;
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/graders/rubric.d.ts
|
|
147
|
+
interface RubricCriterion {
|
|
148
|
+
name: string;
|
|
149
|
+
check: (value: Json | undefined) => boolean;
|
|
150
|
+
}
|
|
151
|
+
interface RubricGraderOptions {
|
|
152
|
+
name?: string;
|
|
153
|
+
/**
|
|
154
|
+
* Minimum fraction of criteria that must pass; default 1 (all).
|
|
155
|
+
* The fraction is also reported as the verdict score.
|
|
156
|
+
*/
|
|
157
|
+
passThreshold?: number;
|
|
158
|
+
}
|
|
159
|
+
declare function rubricGrader(criteria: RubricCriterion[], options?: RubricGraderOptions): Grader;
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/graders/judge.d.ts
|
|
162
|
+
/** The default judge verdict shape. */
|
|
163
|
+
declare const JUDGE_VERDICT_SCHEMA: JsonSchema;
|
|
164
|
+
interface JudgeGraderOptions {
|
|
165
|
+
/** Judge model; required, never defaulted (docs/04 role quality floors). */
|
|
166
|
+
model: ModelSpec;
|
|
167
|
+
/** What to judge: the criteria prose embedded into the judge prompt. */
|
|
168
|
+
instruction: string;
|
|
169
|
+
name?: string;
|
|
170
|
+
/**
|
|
171
|
+
* Custom verdict schema; requires toVerdict. The default schema is
|
|
172
|
+
* JUDGE_VERDICT_SCHEMA with its boolean `passed`.
|
|
173
|
+
*/
|
|
174
|
+
schema?: JsonSchema;
|
|
175
|
+
/** Maps the judge's structured output onto a pass/score pair. */
|
|
176
|
+
toVerdict?: (output: Json) => {
|
|
177
|
+
passed: boolean;
|
|
178
|
+
score?: number;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
declare function judgeGrader(options: JudgeGraderOptions): Grader;
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/committer.d.ts
|
|
184
|
+
interface MeasuredClaimInput {
|
|
185
|
+
/** ULID (or any unique id); the caller mints it deterministically. */
|
|
186
|
+
id: string;
|
|
187
|
+
subject: {
|
|
188
|
+
model: ModelRef;
|
|
189
|
+
effort?: Effort;
|
|
190
|
+
};
|
|
191
|
+
taskClass: TaskClass;
|
|
192
|
+
polarity: "strength" | "weakness";
|
|
193
|
+
/** A typed template render, never a quote from tool output. */
|
|
194
|
+
statement: string;
|
|
195
|
+
metrics: {
|
|
196
|
+
passRate: number;
|
|
197
|
+
n: number;
|
|
198
|
+
graderId: string;
|
|
199
|
+
cost?: number;
|
|
200
|
+
baseline?: {
|
|
201
|
+
model: ModelRef;
|
|
202
|
+
passRate: number;
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
confidence: "high" | "medium" | "low";
|
|
206
|
+
/** ISO date of the sweep run. */
|
|
207
|
+
observedAt: string;
|
|
208
|
+
evidence: EvidenceRef[];
|
|
209
|
+
modelEpoch?: ModelClaim["modelEpoch"];
|
|
210
|
+
}
|
|
211
|
+
interface EvalCommitterOptions {
|
|
212
|
+
/** The dedicated identity recorded on the gate AND the author. */
|
|
213
|
+
committerId: string;
|
|
214
|
+
/** The emitting sweep report; every claim's gate references it. */
|
|
215
|
+
reportId: string;
|
|
216
|
+
/** CAS-rebase attempts (docs/05, 5.4); default 3. */
|
|
217
|
+
attempts?: number;
|
|
218
|
+
}
|
|
219
|
+
/** One measured claim, TTL applied per the docs/05 decay table. */
|
|
220
|
+
declare function evalMeasuredClaim(input: MeasuredClaimInput, committerId: string): ModelClaim;
|
|
221
|
+
/**
|
|
222
|
+
* Commits measured claims through the eval-committer gate with the
|
|
223
|
+
* documented rebase recipe: on a CAS rejection, re-read current() and
|
|
224
|
+
* retry against the fresh version. Returns the committed version.
|
|
225
|
+
*/
|
|
226
|
+
declare function commitEvalMeasured(store: ModelKnowledgeStore, claims: readonly MeasuredClaimInput[], options: EvalCommitterOptions): Promise<number>;
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/canary.d.ts
|
|
229
|
+
interface CanaryProbeSet {
|
|
230
|
+
/** Registered agent profile the probes run under. */
|
|
231
|
+
agentType: string;
|
|
232
|
+
/** The fixed prompts; order matters and enters the fingerprint. */
|
|
233
|
+
prompts: string[];
|
|
234
|
+
}
|
|
235
|
+
/** The committed v1 normalization (OQ-06): NFC, trim, collapse whitespace. */
|
|
236
|
+
declare function normalizeCanaryOutput(output: unknown): string;
|
|
237
|
+
/**
|
|
238
|
+
* Runs the fixed probe set through the ordinary engine and returns the
|
|
239
|
+
* fingerprint. Probes run sequentially in declaration order, one run
|
|
240
|
+
* per probe, so recordings replay deterministically.
|
|
241
|
+
*/
|
|
242
|
+
declare function canaryFingerprint(engine: Engine, probes: CanaryProbeSet): Promise<string>;
|
|
243
|
+
interface CanaryDriftReport {
|
|
244
|
+
model: ModelRef;
|
|
245
|
+
freshFingerprint: string;
|
|
246
|
+
/** Claim ids flipped to stale by this call. */
|
|
247
|
+
flipped: string[];
|
|
248
|
+
/** The committed store version when anything flipped. */
|
|
249
|
+
version?: number;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Flips the model's ACTIVE eval-measured claims to stale when their
|
|
253
|
+
* recorded canary fingerprint differs from the fresh one (docs/05:
|
|
254
|
+
* "a fingerprint change immediately flips the model's eval claims to
|
|
255
|
+
* stale"). Claims without a recorded fingerprint have no baseline and
|
|
256
|
+
* stay untouched (the documented no-probe posture); a second run is
|
|
257
|
+
* an idempotent noop. CAS-rebased like every maintenance commit.
|
|
258
|
+
*/
|
|
259
|
+
declare function flipStaleOnCanaryDrift(store: ModelKnowledgeStore, model: ModelRef, freshFingerprint: string, options?: {
|
|
260
|
+
attempts?: number;
|
|
261
|
+
}): Promise<CanaryDriftReport>;
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/sweeps.d.ts
|
|
264
|
+
/** One fixed pool member; effort is part of the claim subject identity. */
|
|
265
|
+
interface SweepModel {
|
|
266
|
+
model: ModelRef;
|
|
267
|
+
effort?: Effort;
|
|
268
|
+
}
|
|
269
|
+
/** An eval case bound to the taskClass axis of the matrix. */
|
|
270
|
+
interface SweepCase {
|
|
271
|
+
taskClass: TaskClass;
|
|
272
|
+
case: EvalCase;
|
|
273
|
+
}
|
|
274
|
+
interface SweepPool {
|
|
275
|
+
models: SweepModel[];
|
|
276
|
+
cases: SweepCase[];
|
|
277
|
+
}
|
|
278
|
+
interface SweepThresholds {
|
|
279
|
+
/** passRate at or above emits a strength claim; default 0.9. */
|
|
280
|
+
strength: number;
|
|
281
|
+
/** passRate at or below emits a weakness claim; default 0.5. */
|
|
282
|
+
weakness: number;
|
|
283
|
+
}
|
|
284
|
+
interface RunSweepOptions {
|
|
285
|
+
/** Deterministic, caller-minted; every claim's evidence and gate reference it. */
|
|
286
|
+
reportId: string;
|
|
287
|
+
/** The dedicated identity (docs/05, 5.4). */
|
|
288
|
+
committerId: string;
|
|
289
|
+
/** ISO date of the sweep; the TTL table applies from it (no wall clock inside). */
|
|
290
|
+
observedAt: string;
|
|
291
|
+
/**
|
|
292
|
+
* A fresh engine per model cell, routed at that member: the caller
|
|
293
|
+
* owns adapters, budgets, and the VCR posture, so a sweep records
|
|
294
|
+
* and replays like any engine run.
|
|
295
|
+
*/
|
|
296
|
+
engineFor: (member: SweepModel) => Engine | Promise<Engine>;
|
|
297
|
+
/** Mid-band pass rates emit NO claim (uninformative); see defaults. */
|
|
298
|
+
thresholds?: Partial<SweepThresholds>;
|
|
299
|
+
/** Passed through to every suite run (budget, VCR hooks ride the engine). */
|
|
300
|
+
suite?: RunEvalSuiteOptions;
|
|
301
|
+
/** When given, emitted claims commit through the committer identity. */
|
|
302
|
+
store?: ModelKnowledgeStore;
|
|
303
|
+
/**
|
|
304
|
+
* Optional epoch stamp per pool member (capture via the core
|
|
305
|
+
* modelEpochOf; the canary fingerprint rides it when probes ran).
|
|
306
|
+
*/
|
|
307
|
+
modelEpochFor?: (member: SweepModel) => ModelClaim["modelEpoch"];
|
|
308
|
+
}
|
|
309
|
+
interface SweepCellReport {
|
|
310
|
+
model: ModelRef;
|
|
311
|
+
effort?: Effort;
|
|
312
|
+
taskClass: TaskClass;
|
|
313
|
+
passRate: number;
|
|
314
|
+
n: number;
|
|
315
|
+
totalCostUsd: number;
|
|
316
|
+
caseNames: string[];
|
|
317
|
+
}
|
|
318
|
+
interface SweepReport {
|
|
319
|
+
reportId: string;
|
|
320
|
+
observedAt: string;
|
|
321
|
+
cells: SweepCellReport[];
|
|
322
|
+
/** Emitted per the thresholds; committed when a store was given. */
|
|
323
|
+
claims: MeasuredClaimInput[];
|
|
324
|
+
committedVersion?: number;
|
|
325
|
+
}
|
|
326
|
+
declare const SWEEP_THRESHOLD_DEFAULTS: SweepThresholds;
|
|
327
|
+
/**
|
|
328
|
+
* Runs the fixed matrix sequentially in declaration order
|
|
329
|
+
* (deterministic cassette consumption), aggregates per (model,
|
|
330
|
+
* taskClass) cell, emits threshold-crossing claims, and commits them
|
|
331
|
+
* through the eval-committer identity when a store is given.
|
|
332
|
+
*/
|
|
333
|
+
declare function runSweepMatrix(pool: SweepPool, options: RunSweepOptions): Promise<SweepReport>;
|
|
334
|
+
//#endregion
|
|
335
|
+
export { type CanaryDriftReport, type CanaryProbeSet, type EvalCase, type EvalCaseResult, type EvalCommitterOptions, EvalJudgeError, type EvalMatrixReport, type EvalSuiteResult, type GoldenGraderOptions, type Grader, type GraderContext, type GraderVerdict, JUDGE_VERDICT_SCHEMA, type JudgeGraderOptions, type JudgeSpec, type MatrixCell, type MatrixCellReport, type MeasuredClaimInput, type RubricCriterion, type RubricGraderOptions, type RunEvalCaseOptions, type RunEvalSuiteOptions, type RunSweepOptions, SWEEP_THRESHOLD_DEFAULTS, type SweepCase, type SweepCellReport, type SweepModel, type SweepPool, type SweepReport, type SweepThresholds, canaryFingerprint, commitEvalMeasured, evalMeasuredClaim, flipStaleOnCanaryDrift, goldenGrader, judgeGrader, normalizeCanaryOutput, rubricGrader, runEvalCase, runEvalMatrix, runEvalSuite, runSweepMatrix };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { KnowledgeCasError, claimExpiry, defineWorkflow } from "@rulvar/core";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
//#region src/case.ts
|
|
4
|
+
/**
|
|
5
|
+
* @rulvar/evals (M9-T02): EvalCase, the grader contract, and the case and
|
|
6
|
+
* suite runners. A separate quality-measurement package built strictly on
|
|
7
|
+
* the public APIs (L6).
|
|
8
|
+
*
|
|
9
|
+
* Owning spec: docs/09-observability-testing-spec.md, section
|
|
10
|
+
* "@rulvar/evals"; determinism rules in docs/11-testing-strategy.md,
|
|
11
|
+
* section "Eval CI": judge graders run THROUGH the engine, so judge calls
|
|
12
|
+
* are journaled, budgeted, and VCR-recordable, and an eval suite replays
|
|
13
|
+
* deterministically from cassettes with zero live calls.
|
|
14
|
+
*/
|
|
15
|
+
/** Thrown when a judge run does not settle ok. */
|
|
16
|
+
var EvalJudgeError = class extends Error {
|
|
17
|
+
judgeRun;
|
|
18
|
+
status;
|
|
19
|
+
constructor(judgeRun, status, detail) {
|
|
20
|
+
super(`eval judge run '${judgeRun}' settled '${status}'${detail === void 0 ? "" : `: ${detail}`}`);
|
|
21
|
+
this.name = "EvalJudgeError";
|
|
22
|
+
this.judgeRun = judgeRun;
|
|
23
|
+
this.status = status;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Runs one EvalCase on the given engine: the target workflow as its own
|
|
28
|
+
* run, pure graders host-side over the outcome, judge graders through the
|
|
29
|
+
* engine via GraderContext.judge. Grader thrown errors are not absorbed:
|
|
30
|
+
* a grader that cannot grade is a defect of the suite, not a failed case.
|
|
31
|
+
*/
|
|
32
|
+
async function runEvalCase(engine, evalCase, options = {}) {
|
|
33
|
+
const name = options.name ?? evalCase.workflow.name;
|
|
34
|
+
const timing = {};
|
|
35
|
+
const handle = engine.run(evalCase.workflow, evalCase.args, {
|
|
36
|
+
name: `eval:${name}`,
|
|
37
|
+
...options.budgetUsd === void 0 ? {} : { budgetUsd: options.budgetUsd }
|
|
38
|
+
});
|
|
39
|
+
const offStart = handle.on("run:start", (event) => {
|
|
40
|
+
timing.start ??= event.ts;
|
|
41
|
+
});
|
|
42
|
+
const offEnd = handle.on("run:end", (event) => {
|
|
43
|
+
timing.end ??= event.ts;
|
|
44
|
+
});
|
|
45
|
+
const outcome = await handle.result;
|
|
46
|
+
offStart();
|
|
47
|
+
offEnd();
|
|
48
|
+
let judgeCostUsd = 0;
|
|
49
|
+
let judgeOrdinal = 0;
|
|
50
|
+
const context = {
|
|
51
|
+
value: outcome.value,
|
|
52
|
+
outcome,
|
|
53
|
+
async judge(spec) {
|
|
54
|
+
const ordinal = judgeOrdinal;
|
|
55
|
+
judgeOrdinal += 1;
|
|
56
|
+
const judged = await runJudge(engine, `${name}:${ordinal}`, spec, options.judgeBudgetUsd);
|
|
57
|
+
judgeCostUsd += judged.costUsd;
|
|
58
|
+
return judged.output;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const verdicts = [];
|
|
62
|
+
for (const grader of evalCase.graders) verdicts.push(await grader.grade(context));
|
|
63
|
+
const latencyMs = timing.start !== void 0 && timing.end !== void 0 ? Math.max(0, Date.parse(timing.end) - Date.parse(timing.start)) : 0;
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
status: outcome.status,
|
|
67
|
+
passed: outcome.status === "ok" && verdicts.every((verdict) => verdict.passed),
|
|
68
|
+
verdicts,
|
|
69
|
+
costUsd: outcome.cost.totalUsd + judgeCostUsd,
|
|
70
|
+
judgeCostUsd,
|
|
71
|
+
latencyMs,
|
|
72
|
+
usage: outcome.usage,
|
|
73
|
+
...outcome.error === void 0 ? {} : { error: outcome.error }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function runJudge(engine, judgeName, spec, budgetUsd) {
|
|
77
|
+
const workflowName = `eval-judge:${judgeName}`;
|
|
78
|
+
const judgeWorkflow = defineWorkflow({ name: workflowName }, async (ctx) => {
|
|
79
|
+
return await ctx.agent(spec.prompt, {
|
|
80
|
+
model: spec.model,
|
|
81
|
+
schema: spec.schema,
|
|
82
|
+
label: "eval-judge",
|
|
83
|
+
onError: "throw"
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
const outcome = await engine.run(judgeWorkflow, null, {
|
|
87
|
+
name: workflowName,
|
|
88
|
+
...budgetUsd === void 0 ? {} : { budgetUsd }
|
|
89
|
+
}).result;
|
|
90
|
+
if (outcome.status !== "ok") throw new EvalJudgeError(workflowName, outcome.status, outcome.error?.message);
|
|
91
|
+
return {
|
|
92
|
+
output: outcome.value ?? null,
|
|
93
|
+
costUsd: outcome.cost.totalUsd
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Runs cases sequentially (deterministic journal and cassette order) and
|
|
98
|
+
* aggregates. Duplicate workflow names get '#<ordinal>' suffixes so every
|
|
99
|
+
* result row and judge journal is unambiguous.
|
|
100
|
+
*/
|
|
101
|
+
async function runEvalSuite(engine, cases, options = {}) {
|
|
102
|
+
const seen = /* @__PURE__ */ new Map();
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const evalCase of cases) {
|
|
105
|
+
const base = evalCase.workflow.name;
|
|
106
|
+
const ordinal = seen.get(base) ?? 0;
|
|
107
|
+
seen.set(base, ordinal + 1);
|
|
108
|
+
const name = ordinal === 0 ? base : `${base}#${ordinal}`;
|
|
109
|
+
results.push(await runEvalCase(engine, evalCase, {
|
|
110
|
+
name,
|
|
111
|
+
...options.budgetUsd === void 0 ? {} : { budgetUsd: options.budgetUsd },
|
|
112
|
+
...options.judgeBudgetUsd === void 0 ? {} : { judgeBudgetUsd: options.judgeBudgetUsd }
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
results,
|
|
117
|
+
passRate: results.length === 0 ? 0 : results.filter((r) => r.passed).length / results.length,
|
|
118
|
+
totalCostUsd: results.reduce((sum, r) => sum + r.costUsd, 0),
|
|
119
|
+
meanLatencyMs: results.length === 0 ? 0 : results.reduce((sum, r) => sum + r.latencyMs, 0) / results.length
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/matrix.ts
|
|
124
|
+
/**
|
|
125
|
+
* Runs the same case list against every cell's engine, sequentially and
|
|
126
|
+
* in declaration order (deterministic cassette consumption), and reports
|
|
127
|
+
* per-cell aggregates for side-by-side comparison.
|
|
128
|
+
*/
|
|
129
|
+
async function runEvalMatrix(cells, cases, options = {}) {
|
|
130
|
+
const reports = [];
|
|
131
|
+
for (const cell of cells) {
|
|
132
|
+
const suite = await runEvalSuite(await cell.engine(), cases, options);
|
|
133
|
+
reports.push({
|
|
134
|
+
cell: cell.name,
|
|
135
|
+
passRate: suite.passRate,
|
|
136
|
+
totalCostUsd: suite.totalCostUsd,
|
|
137
|
+
meanLatencyMs: suite.meanLatencyMs,
|
|
138
|
+
results: suite.results
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { cells: reports };
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/graders/golden.ts
|
|
145
|
+
function deepEqual(a, b) {
|
|
146
|
+
if (a === b) return true;
|
|
147
|
+
if (a === null || b === null || a === void 0 || b === void 0) return false;
|
|
148
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
149
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
150
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
151
|
+
}
|
|
152
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
153
|
+
const aKeys = Object.keys(a).sort();
|
|
154
|
+
const bKeys = Object.keys(b).sort();
|
|
155
|
+
if (aKeys.length !== bKeys.length || aKeys.some((key, index) => key !== bKeys[index])) return false;
|
|
156
|
+
return aKeys.every((key) => deepEqual(a[key], b[key]));
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
function goldenGrader(expected, options = {}) {
|
|
161
|
+
const name = options.name ?? "golden";
|
|
162
|
+
return {
|
|
163
|
+
name,
|
|
164
|
+
grade(context) {
|
|
165
|
+
const passed = deepEqual(context.value, expected);
|
|
166
|
+
return {
|
|
167
|
+
grader: name,
|
|
168
|
+
passed,
|
|
169
|
+
...passed ? {} : { details: {
|
|
170
|
+
expected,
|
|
171
|
+
actual: context.value ?? null
|
|
172
|
+
} }
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/graders/rubric.ts
|
|
179
|
+
function rubricGrader(criteria, options = {}) {
|
|
180
|
+
const name = options.name ?? "rubric";
|
|
181
|
+
const threshold = options.passThreshold ?? 1;
|
|
182
|
+
return {
|
|
183
|
+
name,
|
|
184
|
+
grade(context) {
|
|
185
|
+
const rows = criteria.map((criterion) => ({
|
|
186
|
+
name: criterion.name,
|
|
187
|
+
passed: criterion.check(context.value)
|
|
188
|
+
}));
|
|
189
|
+
const score = criteria.length === 0 ? 1 : rows.filter((row) => row.passed).length / rows.length;
|
|
190
|
+
return {
|
|
191
|
+
grader: name,
|
|
192
|
+
passed: score >= threshold,
|
|
193
|
+
score,
|
|
194
|
+
details: { criteria: rows }
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/graders/judge.ts
|
|
201
|
+
/** The default judge verdict shape. */
|
|
202
|
+
const JUDGE_VERDICT_SCHEMA = {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
passed: { type: "boolean" },
|
|
206
|
+
reasoning: { type: "string" }
|
|
207
|
+
},
|
|
208
|
+
required: ["passed"],
|
|
209
|
+
additionalProperties: false
|
|
210
|
+
};
|
|
211
|
+
function defaultToVerdict(output) {
|
|
212
|
+
return { passed: typeof output === "object" && output !== null && !Array.isArray(output) && output.passed === true };
|
|
213
|
+
}
|
|
214
|
+
function judgePrompt(instruction, value) {
|
|
215
|
+
return [
|
|
216
|
+
"You are an evaluation judge. Judge the candidate output below against the instruction.",
|
|
217
|
+
"",
|
|
218
|
+
`Instruction: ${instruction}`,
|
|
219
|
+
"",
|
|
220
|
+
"Candidate output (JSON):",
|
|
221
|
+
JSON.stringify(value ?? null),
|
|
222
|
+
"",
|
|
223
|
+
"Return a verdict object matching the response schema."
|
|
224
|
+
].join("\n");
|
|
225
|
+
}
|
|
226
|
+
function judgeGrader(options) {
|
|
227
|
+
const name = options.name ?? "judge";
|
|
228
|
+
if (options.schema !== void 0 && options.toVerdict === void 0) throw new Error(`judgeGrader '${name}': a custom schema requires toVerdict`);
|
|
229
|
+
const toVerdict = options.toVerdict ?? defaultToVerdict;
|
|
230
|
+
const schema = options.schema ?? JUDGE_VERDICT_SCHEMA;
|
|
231
|
+
return {
|
|
232
|
+
name,
|
|
233
|
+
async grade(context) {
|
|
234
|
+
if (context.outcome.status !== "ok") return {
|
|
235
|
+
grader: name,
|
|
236
|
+
passed: false,
|
|
237
|
+
details: { skipped: `target run settled '${context.outcome.status}'` }
|
|
238
|
+
};
|
|
239
|
+
const output = await context.judge({
|
|
240
|
+
model: options.model,
|
|
241
|
+
prompt: judgePrompt(options.instruction, context.value),
|
|
242
|
+
schema
|
|
243
|
+
});
|
|
244
|
+
const verdict = toVerdict(output);
|
|
245
|
+
return {
|
|
246
|
+
grader: name,
|
|
247
|
+
passed: verdict.passed,
|
|
248
|
+
...verdict.score === void 0 ? {} : { score: verdict.score },
|
|
249
|
+
details: { output }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/committer.ts
|
|
256
|
+
/**
|
|
257
|
+
* The eval-committer identity (M11-T01; docs/05, sections "Data model"
|
|
258
|
+
* and "Commit discipline"). The pipeline-side commit path: builds
|
|
259
|
+
* eval-committer-gated ops (the coherence square: class eval-measured,
|
|
260
|
+
* author eval-pipeline, metrics present) and commits them with the
|
|
261
|
+
* documented CAS-rebase recipe. Humans never call this; their path is
|
|
262
|
+
* the human gate and it structurally cannot carry metrics.
|
|
263
|
+
*/
|
|
264
|
+
/** One measured claim, TTL applied per the docs/05 decay table. */
|
|
265
|
+
function evalMeasuredClaim(input, committerId) {
|
|
266
|
+
return {
|
|
267
|
+
id: input.id,
|
|
268
|
+
subject: input.subject,
|
|
269
|
+
taskClass: input.taskClass,
|
|
270
|
+
polarity: input.polarity,
|
|
271
|
+
statement: input.statement,
|
|
272
|
+
class: "eval-measured",
|
|
273
|
+
status: "active",
|
|
274
|
+
evidence: input.evidence,
|
|
275
|
+
metrics: input.metrics,
|
|
276
|
+
confidence: input.confidence,
|
|
277
|
+
observedAt: input.observedAt,
|
|
278
|
+
expiresAt: claimExpiry("eval-measured", input.polarity, input.observedAt),
|
|
279
|
+
...input.modelEpoch === void 0 ? {} : { modelEpoch: input.modelEpoch },
|
|
280
|
+
author: {
|
|
281
|
+
kind: "eval-pipeline",
|
|
282
|
+
id: committerId
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Commits measured claims through the eval-committer gate with the
|
|
288
|
+
* documented rebase recipe: on a CAS rejection, re-read current() and
|
|
289
|
+
* retry against the fresh version. Returns the committed version.
|
|
290
|
+
*/
|
|
291
|
+
async function commitEvalMeasured(store, claims, options) {
|
|
292
|
+
const gate = {
|
|
293
|
+
kind: "eval-committer",
|
|
294
|
+
committerId: options.committerId,
|
|
295
|
+
reportId: options.reportId
|
|
296
|
+
};
|
|
297
|
+
const ops = claims.map((input) => ({
|
|
298
|
+
op: "add",
|
|
299
|
+
claim: evalMeasuredClaim(input, options.committerId),
|
|
300
|
+
gate
|
|
301
|
+
}));
|
|
302
|
+
const attempts = options.attempts ?? 3;
|
|
303
|
+
let lastCas;
|
|
304
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
305
|
+
const snapshot = await store.current();
|
|
306
|
+
try {
|
|
307
|
+
return await store.commit(ops, snapshot.version);
|
|
308
|
+
} catch (thrown) {
|
|
309
|
+
if (thrown instanceof KnowledgeCasError) {
|
|
310
|
+
lastCas = thrown;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
throw thrown;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
throw lastCas ?? /* @__PURE__ */ new Error("commitEvalMeasured: unreachable");
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/canary.ts
|
|
320
|
+
/**
|
|
321
|
+
* The canary fingerprint (M11-T04; docs/05, section "Grounding and
|
|
322
|
+
* decay"; OQ-06). The optional compensation for silent alias
|
|
323
|
+
* re-pointing that modelEpoch honestly cannot catch: a FIXED probe set
|
|
324
|
+
* at temperature 0, run through the ordinary engine (journaled,
|
|
325
|
+
* budgeted, VCR-recordable), hashed over normalized outputs. A
|
|
326
|
+
* fingerprint change flips the model's eval claims to stale in one
|
|
327
|
+
* command.
|
|
328
|
+
*
|
|
329
|
+
* The committed v1 design (closing OQ-06): the probe set is CALLER
|
|
330
|
+
* data (fixed, versioned alongside the store); normalization is NFC,
|
|
331
|
+
* trim, and whitespace collapse per output; the fingerprint is the
|
|
332
|
+
* sha256 of the JCS-serialized normalized output array, prefixed with
|
|
333
|
+
* the probe count so a probe-set edit never collides with drift.
|
|
334
|
+
*/
|
|
335
|
+
/** The committed v1 normalization (OQ-06): NFC, trim, collapse whitespace. */
|
|
336
|
+
function normalizeCanaryOutput(output) {
|
|
337
|
+
return (typeof output === "string" ? output : JSON.stringify(output ?? null)).normalize("NFC").trim().replace(/\s+/gu, " ");
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Runs the fixed probe set through the ordinary engine and returns the
|
|
341
|
+
* fingerprint. Probes run sequentially in declaration order, one run
|
|
342
|
+
* per probe, so recordings replay deterministically.
|
|
343
|
+
*/
|
|
344
|
+
async function canaryFingerprint(engine, probes) {
|
|
345
|
+
const outputs = [];
|
|
346
|
+
for (const [index, prompt] of probes.prompts.entries()) {
|
|
347
|
+
const workflow = defineWorkflow({ name: `kb-canary:${String(index)}` }, async (ctx) => await ctx.agent(prompt, { agentType: probes.agentType }));
|
|
348
|
+
const outcome = await engine.run(workflow, null).result;
|
|
349
|
+
outputs.push(outcome.status === "ok" ? normalizeCanaryOutput(outcome.value) : `!${outcome.status}`);
|
|
350
|
+
}
|
|
351
|
+
const body = JSON.stringify([probes.prompts.length, outputs]);
|
|
352
|
+
return createHash("sha256").update(body, "utf8").digest("hex");
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Flips the model's ACTIVE eval-measured claims to stale when their
|
|
356
|
+
* recorded canary fingerprint differs from the fresh one (docs/05:
|
|
357
|
+
* "a fingerprint change immediately flips the model's eval claims to
|
|
358
|
+
* stale"). Claims without a recorded fingerprint have no baseline and
|
|
359
|
+
* stay untouched (the documented no-probe posture); a second run is
|
|
360
|
+
* an idempotent noop. CAS-rebased like every maintenance commit.
|
|
361
|
+
*/
|
|
362
|
+
async function flipStaleOnCanaryDrift(store, model, freshFingerprint, options) {
|
|
363
|
+
const attempts = options?.attempts ?? 3;
|
|
364
|
+
let lastCas;
|
|
365
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
366
|
+
const snapshot = await store.current();
|
|
367
|
+
const drifted = snapshot.claims.filter((claim) => claim.status === "active" && claim.class === "eval-measured" && claim.subject.model === model && claim.modelEpoch?.canaryFingerprint !== void 0 && claim.modelEpoch.canaryFingerprint !== freshFingerprint);
|
|
368
|
+
if (drifted.length === 0) return {
|
|
369
|
+
model,
|
|
370
|
+
freshFingerprint,
|
|
371
|
+
flipped: []
|
|
372
|
+
};
|
|
373
|
+
const ops = drifted.map((claim) => ({
|
|
374
|
+
op: "mark_stale",
|
|
375
|
+
claimId: claim.id,
|
|
376
|
+
reason: "canary-drift"
|
|
377
|
+
}));
|
|
378
|
+
try {
|
|
379
|
+
const version = await store.commit(ops, snapshot.version);
|
|
380
|
+
return {
|
|
381
|
+
model,
|
|
382
|
+
freshFingerprint,
|
|
383
|
+
flipped: drifted.map((claim) => claim.id),
|
|
384
|
+
version
|
|
385
|
+
};
|
|
386
|
+
} catch (thrown) {
|
|
387
|
+
if (thrown instanceof KnowledgeCasError) {
|
|
388
|
+
lastCas = thrown;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
throw thrown;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
throw lastCas ?? /* @__PURE__ */ new Error("flipStaleOnCanaryDrift: unreachable");
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/sweeps.ts
|
|
398
|
+
const SWEEP_THRESHOLD_DEFAULTS = {
|
|
399
|
+
strength: .9,
|
|
400
|
+
weakness: .5
|
|
401
|
+
};
|
|
402
|
+
/** Deterministic claim id: report-scoped, readable, collision-free. */
|
|
403
|
+
function claimIdOf(reportId, member, taskClass) {
|
|
404
|
+
const effort = member.effort === void 0 ? "" : `@${member.effort}`;
|
|
405
|
+
return `${reportId}/${member.model}${effort}/${taskClass}`;
|
|
406
|
+
}
|
|
407
|
+
/** The typed statement template: never a quote from tool output. */
|
|
408
|
+
function statementOf(cell, polarity) {
|
|
409
|
+
const rate = cell.passRate.toFixed(2);
|
|
410
|
+
const band = polarity === "strength" ? "at or above the strength band" : "in the weakness band";
|
|
411
|
+
return `sweep passRate ${rate} over ${String(cell.n)} ${cell.taskClass} case${cell.n === 1 ? "" : "s"}: ${band}`;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Runs the fixed matrix sequentially in declaration order
|
|
415
|
+
* (deterministic cassette consumption), aggregates per (model,
|
|
416
|
+
* taskClass) cell, emits threshold-crossing claims, and commits them
|
|
417
|
+
* through the eval-committer identity when a store is given.
|
|
418
|
+
*/
|
|
419
|
+
async function runSweepMatrix(pool, options) {
|
|
420
|
+
const thresholds = {
|
|
421
|
+
...SWEEP_THRESHOLD_DEFAULTS,
|
|
422
|
+
...options.thresholds
|
|
423
|
+
};
|
|
424
|
+
const byTaskClass = /* @__PURE__ */ new Map();
|
|
425
|
+
for (const entry of pool.cases) {
|
|
426
|
+
const bucket = byTaskClass.get(entry.taskClass) ?? [];
|
|
427
|
+
bucket.push(entry);
|
|
428
|
+
byTaskClass.set(entry.taskClass, bucket);
|
|
429
|
+
}
|
|
430
|
+
const cells = [];
|
|
431
|
+
const claims = [];
|
|
432
|
+
for (const member of pool.models) {
|
|
433
|
+
const engine = await options.engineFor(member);
|
|
434
|
+
for (const [taskClass, bucket] of byTaskClass) {
|
|
435
|
+
const suite = await runEvalSuite(engine, bucket.map((entry) => entry.case), options.suite ?? {});
|
|
436
|
+
const cell = {
|
|
437
|
+
model: member.model,
|
|
438
|
+
...member.effort === void 0 ? {} : { effort: member.effort },
|
|
439
|
+
taskClass,
|
|
440
|
+
passRate: suite.passRate,
|
|
441
|
+
n: suite.results.length,
|
|
442
|
+
totalCostUsd: suite.totalCostUsd,
|
|
443
|
+
caseNames: suite.results.map((result) => result.name)
|
|
444
|
+
};
|
|
445
|
+
cells.push(cell);
|
|
446
|
+
const polarity = cell.passRate >= thresholds.strength ? "strength" : cell.passRate <= thresholds.weakness ? "weakness" : void 0;
|
|
447
|
+
if (polarity !== void 0 && cell.n > 0) {
|
|
448
|
+
const epoch = options.modelEpochFor?.(member);
|
|
449
|
+
claims.push({
|
|
450
|
+
id: claimIdOf(options.reportId, member, taskClass),
|
|
451
|
+
subject: {
|
|
452
|
+
model: member.model,
|
|
453
|
+
...member.effort === void 0 ? {} : { effort: member.effort }
|
|
454
|
+
},
|
|
455
|
+
taskClass,
|
|
456
|
+
polarity,
|
|
457
|
+
statement: statementOf(cell, polarity),
|
|
458
|
+
metrics: {
|
|
459
|
+
passRate: cell.passRate,
|
|
460
|
+
n: cell.n,
|
|
461
|
+
graderId: "eval-suite"
|
|
462
|
+
},
|
|
463
|
+
confidence: cell.n >= 20 ? "high" : cell.n >= 5 ? "medium" : "low",
|
|
464
|
+
observedAt: options.observedAt,
|
|
465
|
+
evidence: [{
|
|
466
|
+
kind: "eval",
|
|
467
|
+
reportId: options.reportId,
|
|
468
|
+
caseIds: cell.caseNames
|
|
469
|
+
}],
|
|
470
|
+
...epoch === void 0 ? {} : { modelEpoch: epoch }
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const report = {
|
|
476
|
+
reportId: options.reportId,
|
|
477
|
+
observedAt: options.observedAt,
|
|
478
|
+
cells,
|
|
479
|
+
claims
|
|
480
|
+
};
|
|
481
|
+
if (options.store !== void 0 && claims.length > 0) report.committedVersion = await commitEvalMeasured(options.store, claims, {
|
|
482
|
+
committerId: options.committerId,
|
|
483
|
+
reportId: options.reportId
|
|
484
|
+
});
|
|
485
|
+
return report;
|
|
486
|
+
}
|
|
487
|
+
//#endregion
|
|
488
|
+
export { EvalJudgeError, JUDGE_VERDICT_SCHEMA, SWEEP_THRESHOLD_DEFAULTS, canaryFingerprint, commitEvalMeasured, evalMeasuredClaim, flipStaleOnCanaryDrift, goldenGrader, judgeGrader, normalizeCanaryOutput, rubricGrader, runEvalCase, runEvalMatrix, runEvalSuite, runSweepMatrix };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rulvar/evals",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "rulvar evals: eval cases, golden outputs, rubric and judge graders, matrix sweeps, canary fingerprint.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.12.0"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@rulvar/testing": "1.0.0",
|
|
26
|
+
"@rulvar/core": "1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.20.0",
|
|
30
|
+
"tsdown": "^0.22.3",
|
|
31
|
+
"typescript": "~6.0.3"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/o-stepper/rulvar.git",
|
|
36
|
+
"directory": "packages/evals"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://rulvar.com",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/o-stepper/rulvar/issues"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"lint": "eslint .",
|
|
46
|
+
"pack-check": "publint --pack pnpm && attw --pack . --profile esm-only"
|
|
47
|
+
}
|
|
48
|
+
}
|