@rulvar/evals 1.16.2 → 1.18.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/dist/index.d.ts +181 -9
- package/dist/index.js +226 -24
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { CompiledWorkflow, DeclaredLadder, Effort, Engine, EvidenceRef, Json, JsonSchema, KnowledgeSnapshot, ModelClaim, ModelKnowledgeStore, ModelRef, ModelSpec, RunOutcome, SchemaSpec, TaskClass, Usage, WireError, Workflow } from "@rulvar/core";
|
|
2
2
|
|
|
3
|
+
//#region src/envelope.d.ts
|
|
4
|
+
/** Thrown when authorizing a run's ceiling would exceed the envelope. */
|
|
5
|
+
declare class SweepBudgetError extends Error {
|
|
6
|
+
/** What was about to start, e.g. `eval target 'sweep-math'`. */
|
|
7
|
+
readonly runLabel: string;
|
|
8
|
+
/** The per-run ceiling that did not fit. */
|
|
9
|
+
readonly ceilingUsd: number;
|
|
10
|
+
/** Total already authorized before this refusal. */
|
|
11
|
+
readonly authorizedUsd: number;
|
|
12
|
+
readonly maxTotalUsd: number;
|
|
13
|
+
constructor(runLabel: string, ceilingUsd: number, authorizedUsd: number, maxTotalUsd: number);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One envelope bounds one whole sweep invocation: share the instance
|
|
17
|
+
* across the canary loop and runSweepMatrix so canary, target, and
|
|
18
|
+
* judge runs all draw from the same remainder.
|
|
19
|
+
*/
|
|
20
|
+
declare class SpendEnvelope {
|
|
21
|
+
readonly maxTotalUsd: number;
|
|
22
|
+
private readonly maxMicroUsd;
|
|
23
|
+
private authorizedMicroUsd;
|
|
24
|
+
constructor(maxTotalUsd: number);
|
|
25
|
+
/** Total authorized so far (debit-only; never decreases). */
|
|
26
|
+
get authorizedUsd(): number;
|
|
27
|
+
get remainingUsd(): number;
|
|
28
|
+
/**
|
|
29
|
+
* Authorizes one run's immutable ceiling or throws SweepBudgetError.
|
|
30
|
+
* An unbounded run cannot be authorized: under an envelope every run
|
|
31
|
+
* MUST carry an explicit positive ceiling, otherwise the aggregate
|
|
32
|
+
* bound would be unaccountable.
|
|
33
|
+
*/
|
|
34
|
+
authorize(ceilingUsd: number | undefined, runLabel: string): void;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
3
37
|
//#region src/case.d.ts
|
|
4
38
|
/**
|
|
5
39
|
* One quality-measurement case. The shape is the
|
|
@@ -71,6 +105,20 @@ interface EvalCaseResult {
|
|
|
71
105
|
/** The target run's normalized usage. */
|
|
72
106
|
usage: Usage;
|
|
73
107
|
error?: WireError;
|
|
108
|
+
/**
|
|
109
|
+
* Present when grading stopped for a BUDGET reason (v1.17.0 review
|
|
110
|
+
* P1-5): the judge run hit its own per-run ceiling
|
|
111
|
+
* ('judge-exhausted') or the aggregate envelope refused a judge run
|
|
112
|
+
* before it started ('judge-refused'). The paid target evidence and
|
|
113
|
+
* its cost stay on this row, but the case can never count as passed
|
|
114
|
+
* and its cell emits no claim. Unexpected grader errors still throw:
|
|
115
|
+
* a grader that cannot grade for non-budget reasons is a defect of
|
|
116
|
+
* the suite, not a budget event.
|
|
117
|
+
*/
|
|
118
|
+
incomplete?: {
|
|
119
|
+
reason: "judge-exhausted" | "judge-refused";
|
|
120
|
+
detail: string;
|
|
121
|
+
};
|
|
74
122
|
}
|
|
75
123
|
interface RunEvalCaseOptions {
|
|
76
124
|
/** Display-name override; defaults to the workflow name. */
|
|
@@ -79,12 +127,21 @@ interface RunEvalCaseOptions {
|
|
|
79
127
|
budgetUsd?: number;
|
|
80
128
|
/** Run ceiling for each judge run. */
|
|
81
129
|
judgeBudgetUsd?: number;
|
|
130
|
+
/**
|
|
131
|
+
* Aggregate debit-only envelope (v1.16.2 review P1-2): every target
|
|
132
|
+
* and judge run authorizes its ceiling here BEFORE starting, and an
|
|
133
|
+
* envelope requires the matching per-run ceiling to be set. A
|
|
134
|
+
* refusal throws SweepBudgetError before any provider work.
|
|
135
|
+
*/
|
|
136
|
+
envelope?: SpendEnvelope;
|
|
82
137
|
}
|
|
83
138
|
/** Thrown when a judge run does not settle ok. */
|
|
84
139
|
declare class EvalJudgeError extends Error {
|
|
85
140
|
readonly judgeRun: string;
|
|
86
141
|
readonly status: RunOutcome<Json>["status"];
|
|
87
|
-
|
|
142
|
+
/** What the failing judge run actually spent (honest cost accounting). */
|
|
143
|
+
readonly costUsd: number;
|
|
144
|
+
constructor(judgeRun: string, status: RunOutcome<Json>["status"], detail?: string, costUsd?: number);
|
|
88
145
|
}
|
|
89
146
|
/**
|
|
90
147
|
* Runs one EvalCase on the given engine: the target workflow as its own
|
|
@@ -96,15 +153,33 @@ declare function runEvalCase(engine: Engine, evalCase: EvalCase, options?: RunEv
|
|
|
96
153
|
/** Aggregate view of a suite run. */
|
|
97
154
|
interface EvalSuiteResult {
|
|
98
155
|
results: EvalCaseResult[];
|
|
99
|
-
/** Fraction of
|
|
156
|
+
/** Fraction of result rows with passed true; 0 for an empty suite. */
|
|
100
157
|
passRate: number;
|
|
101
158
|
totalCostUsd: number;
|
|
102
|
-
/** Arithmetic mean over
|
|
159
|
+
/** Arithmetic mean over result rows; 0 for an empty suite. */
|
|
103
160
|
meanLatencyMs: number;
|
|
161
|
+
/** Cases the caller asked for. */
|
|
162
|
+
plannedN: number;
|
|
163
|
+
/** Result rows actually produced (equals results.length). */
|
|
164
|
+
completedN: number;
|
|
165
|
+
/**
|
|
166
|
+
* Present when the aggregate envelope refused a TARGET run before it
|
|
167
|
+
* started (v1.17.0 review P1-5). The suite stops there and returns
|
|
168
|
+
* everything already measured instead of throwing: completed rows,
|
|
169
|
+
* their costs, and their names survive. Judge refusals never appear
|
|
170
|
+
* here; they normalize into the owning row's `incomplete` marker.
|
|
171
|
+
*/
|
|
172
|
+
refusal?: {
|
|
173
|
+
runLabel: string;
|
|
174
|
+
atCase: string;
|
|
175
|
+
detail: string;
|
|
176
|
+
};
|
|
104
177
|
}
|
|
105
178
|
interface RunEvalSuiteOptions {
|
|
106
179
|
budgetUsd?: number;
|
|
107
180
|
judgeBudgetUsd?: number;
|
|
181
|
+
/** See RunEvalCaseOptions.envelope; shared across every case of the suite. */
|
|
182
|
+
envelope?: SpendEnvelope;
|
|
108
183
|
}
|
|
109
184
|
/**
|
|
110
185
|
* Runs cases sequentially (deterministic journal and cassette order) and
|
|
@@ -232,14 +307,59 @@ interface CanaryProbeSet {
|
|
|
232
307
|
/** The fixed prompts; order matters and enters the fingerprint. */
|
|
233
308
|
prompts: string[];
|
|
234
309
|
}
|
|
310
|
+
interface CanaryRunOptions {
|
|
311
|
+
/**
|
|
312
|
+
* Immutable ceiling per probe run (v1.16.2 review P1-2): every probe
|
|
313
|
+
* is an ordinary paid engine run and gets its own recorded
|
|
314
|
+
* RunMeta.budgetUsd.
|
|
315
|
+
*/
|
|
316
|
+
budgetUsd?: number;
|
|
317
|
+
/**
|
|
318
|
+
* Aggregate debit-only envelope shared with the surrounding sweep;
|
|
319
|
+
* each probe authorizes budgetUsd BEFORE running, and an envelope
|
|
320
|
+
* requires budgetUsd to be set.
|
|
321
|
+
*/
|
|
322
|
+
envelope?: SpendEnvelope;
|
|
323
|
+
}
|
|
324
|
+
interface CanaryReport {
|
|
325
|
+
fingerprint: string;
|
|
326
|
+
/**
|
|
327
|
+
* True only when every probe settled ok. A fingerprint containing a
|
|
328
|
+
* non-ok probe status is a measurement artifact (budget exhaustion,
|
|
329
|
+
* an envelope refusal, transient provider failure), NOT evidence of
|
|
330
|
+
* model drift: never feed it to flipStaleOnCanaryDrift.
|
|
331
|
+
*/
|
|
332
|
+
allOk: boolean;
|
|
333
|
+
/**
|
|
334
|
+
* One row per probe; 'refused' means the aggregate envelope refused
|
|
335
|
+
* the probe before it started (v1.17.0 review P1-5): the loop keeps
|
|
336
|
+
* walking so completed probe evidence survives, and allOk is false.
|
|
337
|
+
*/
|
|
338
|
+
probes: Array<{
|
|
339
|
+
prompt: string;
|
|
340
|
+
status: RunOutcome<unknown>["status"] | "refused";
|
|
341
|
+
}>;
|
|
342
|
+
}
|
|
235
343
|
/** The committed v1 normalization (OQ-06): NFC, trim, collapse whitespace. */
|
|
236
344
|
declare function normalizeCanaryOutput(output: unknown): string;
|
|
237
345
|
/**
|
|
238
|
-
* Runs the fixed probe set through the ordinary engine
|
|
239
|
-
*
|
|
240
|
-
*
|
|
346
|
+
* Runs the fixed probe set through the ordinary engine. Probes run
|
|
347
|
+
* sequentially in declaration order, one run per probe, so recordings
|
|
348
|
+
* replay deterministically. Each probe run carries the optional
|
|
349
|
+
* immutable ceiling (options.budgetUsd) and authorizes it against the
|
|
350
|
+
* optional envelope before starting; an envelope refusal records the
|
|
351
|
+
* probe as 'refused' and keeps walking instead of throwing away the
|
|
352
|
+
* completed probes. A non-ok or refused probe enters the fingerprint
|
|
353
|
+
* as `!status` and clears allOk: callers gate drift flipping on allOk,
|
|
354
|
+
* because a budget-starved or transiently failing probe fingerprints
|
|
355
|
+
* differently without the model having drifted.
|
|
241
356
|
*/
|
|
242
|
-
declare function
|
|
357
|
+
declare function runCanary(engine: Engine, probes: CanaryProbeSet, options?: CanaryRunOptions): Promise<CanaryReport>;
|
|
358
|
+
/**
|
|
359
|
+
* The fingerprint alone (the pre-v1.16.2-review surface, kept
|
|
360
|
+
* compatible). Prefer runCanary: its allOk is the drift-flip gate.
|
|
361
|
+
*/
|
|
362
|
+
declare function canaryFingerprint(engine: Engine, probes: CanaryProbeSet, options?: CanaryRunOptions): Promise<string>;
|
|
243
363
|
interface CanaryDriftReport {
|
|
244
364
|
model: ModelRef;
|
|
245
365
|
freshFingerprint: string;
|
|
@@ -253,7 +373,13 @@ interface CanaryDriftReport {
|
|
|
253
373
|
* recorded canary fingerprint differs from the fresh one. Claims
|
|
254
374
|
* without a recorded fingerprint have no baseline and
|
|
255
375
|
* stay untouched (the documented no-probe posture); a second run is
|
|
256
|
-
* an idempotent noop. CAS-rebased like every maintenance commit
|
|
376
|
+
* an idempotent noop. CAS-rebased like every maintenance commit; the
|
|
377
|
+
* retries run no engine work and pay nothing.
|
|
378
|
+
*
|
|
379
|
+
* Only pass fingerprints from an allOk probe set (runCanary): a
|
|
380
|
+
* fingerprint containing a `!status` probe differs from any healthy
|
|
381
|
+
* baseline by construction, and flipping on it would blame the model
|
|
382
|
+
* for a budget ceiling or a transient provider failure.
|
|
257
383
|
*/
|
|
258
384
|
declare function flipStaleOnCanaryDrift(store: ModelKnowledgeStore, model: ModelRef, freshFingerprint: string, options?: {
|
|
259
385
|
attempts?: number;
|
|
@@ -297,6 +423,20 @@ interface RunSweepOptions {
|
|
|
297
423
|
thresholds?: Partial<SweepThresholds>;
|
|
298
424
|
/** Passed through to every suite run (budget, VCR hooks ride the engine). */
|
|
299
425
|
suite?: RunEvalSuiteOptions;
|
|
426
|
+
/**
|
|
427
|
+
* Aggregate debit-only envelope over the WHOLE matrix (v1.16.2
|
|
428
|
+
* review P1-2): every target and judge run authorizes its immutable
|
|
429
|
+
* ceiling before starting, so the pool times cases times judge-call
|
|
430
|
+
* product cannot exceed it, falsification pool growth included. An
|
|
431
|
+
* envelope requires suite.budgetUsd (and suite.judgeBudgetUsd once a
|
|
432
|
+
* grader judges). Refusals are monotone (v1.17.0 review P1-5): a
|
|
433
|
+
* refused target stops that cell's walk but everything already
|
|
434
|
+
* measured stays on the cell (n, costs, caseNames), judge refusals
|
|
435
|
+
* normalize into their row's incomplete marker, and an incomplete
|
|
436
|
+
* cell emits NO claim. Share the instance with the canary loop so
|
|
437
|
+
* probes draw from the same remainder.
|
|
438
|
+
*/
|
|
439
|
+
envelope?: SpendEnvelope;
|
|
300
440
|
/** When given, emitted claims commit through the committer identity. */
|
|
301
441
|
store?: ModelKnowledgeStore;
|
|
302
442
|
/**
|
|
@@ -310,9 +450,41 @@ interface SweepCellReport {
|
|
|
310
450
|
effort?: Effort;
|
|
311
451
|
taskClass: TaskClass;
|
|
312
452
|
passRate: number;
|
|
453
|
+
/** Result rows actually measured (completed count). */
|
|
313
454
|
n: number;
|
|
455
|
+
/**
|
|
456
|
+
* Cases this cell was asked to measure (v1.17.0 review P1-5). A cell
|
|
457
|
+
* with n < plannedN is incomplete: what ran stays reported, and the
|
|
458
|
+
* cell emits no claim.
|
|
459
|
+
*/
|
|
460
|
+
plannedN: number;
|
|
314
461
|
totalCostUsd: number;
|
|
315
462
|
caseNames: string[];
|
|
463
|
+
/**
|
|
464
|
+
* Count of case results whose TARGET run settled 'exhausted' (its
|
|
465
|
+
* per-run ceiling, not the envelope). A budget-starved measurement
|
|
466
|
+
* must not become a model belief, so any exhausted target suppresses
|
|
467
|
+
* the cell's claim even when the degraded passRate crosses a
|
|
468
|
+
* threshold: the alternative is committing a false weakness that
|
|
469
|
+
* blames the model for the ceiling.
|
|
470
|
+
*/
|
|
471
|
+
exhaustedRuns?: number;
|
|
472
|
+
/**
|
|
473
|
+
* Count of result rows whose grading stopped on a judge budget event
|
|
474
|
+
* (per-run judge ceiling or envelope refusal of a judge run). The
|
|
475
|
+
* paid target evidence stays on those rows; the cell emits no claim.
|
|
476
|
+
*/
|
|
477
|
+
judgeIncompleteRuns?: number;
|
|
478
|
+
/**
|
|
479
|
+
* The aggregate envelope refused a TARGET run of this cell before it
|
|
480
|
+
* started; everything measured up to that point stays reported and
|
|
481
|
+
* the cell emits no claim.
|
|
482
|
+
*/
|
|
483
|
+
envelopeExhausted?: true;
|
|
484
|
+
/** Why the cell is incomplete, when it is. */
|
|
485
|
+
incompleteReason?: "envelope-exhausted" | "judge-exhausted" | "judge-refused";
|
|
486
|
+
/** The refused run's label, when the envelope refused one. */
|
|
487
|
+
refusedRunLabel?: string;
|
|
316
488
|
}
|
|
317
489
|
interface SweepReport {
|
|
318
490
|
reportId: string;
|
|
@@ -417,4 +589,4 @@ declare function runValueCheckpoint(checkpointPool: CheckpointPool, options: Run
|
|
|
417
589
|
/** The deterministic render for the M12 gate docs amendment. */
|
|
418
590
|
declare function renderCheckpointReport(report: CheckpointReport): string;
|
|
419
591
|
//#endregion
|
|
420
|
-
export { type CanaryDriftReport, type CanaryProbeSet, type CheckpointArm, type CheckpointCell, type CheckpointLadder, type CheckpointPool, type CheckpointReport, type CriterionOneReport, type CriterionTwoReport, 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 OrchestratedCase, type RubricCriterion, type RubricGraderOptions, type RunCheckpointOptions, 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, renderCheckpointReport, rubricGrader, runEvalCase, runEvalMatrix, runEvalSuite, runSweepMatrix, runValueCheckpoint, rungRuleHolds };
|
|
592
|
+
export { type CanaryDriftReport, type CanaryProbeSet, type CanaryReport, type CanaryRunOptions, type CheckpointArm, type CheckpointCell, type CheckpointLadder, type CheckpointPool, type CheckpointReport, type CriterionOneReport, type CriterionTwoReport, 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 OrchestratedCase, type RubricCriterion, type RubricGraderOptions, type RunCheckpointOptions, type RunEvalCaseOptions, type RunEvalSuiteOptions, type RunSweepOptions, SWEEP_THRESHOLD_DEFAULTS, SpendEnvelope, SweepBudgetError, type SweepCase, type SweepCellReport, type SweepModel, type SweepPool, type SweepReport, type SweepThresholds, canaryFingerprint, commitEvalMeasured, evalMeasuredClaim, flipStaleOnCanaryDrift, goldenGrader, judgeGrader, normalizeCanaryOutput, renderCheckpointReport, rubricGrader, runCanary, runEvalCase, runEvalMatrix, runEvalSuite, runSweepMatrix, runValueCheckpoint, rungRuleHolds };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,100 @@
|
|
|
1
|
-
import { KnowledgeCasError, claimExpiry, compileVerifiedLayer, defineWorkflow } from "@rulvar/core";
|
|
1
|
+
import { ConfigError, KnowledgeCasError, claimExpiry, compileVerifiedLayer, defineWorkflow } from "@rulvar/core";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
//#region src/envelope.ts
|
|
4
|
+
/**
|
|
5
|
+
* The debit-only aggregate spend envelope (v1.16.2 review P1-2). A
|
|
6
|
+
* sweep multiplies paid runs: pool members times cases for targets,
|
|
7
|
+
* one judge run per GraderContext.judge call, one canary run per
|
|
8
|
+
* probe per member, and the falsification union can grow the pool
|
|
9
|
+
* beyond the config. Per-run ceilings alone do not bound that
|
|
10
|
+
* product, so the envelope authorizes each run's IMMUTABLE ceiling
|
|
11
|
+
* BEFORE the run starts: a run whose ceiling does not fit the
|
|
12
|
+
* remainder is refused before any provider work.
|
|
13
|
+
*
|
|
14
|
+
* Authorizations are never returned: not when a run completes under
|
|
15
|
+
* its ceiling, not on CAS retries (they run no paid work), and not on
|
|
16
|
+
* replay. A replayed run authorizes exactly like a fresh one and then
|
|
17
|
+
* spends nothing; the envelope bounds the authorized worst case, not
|
|
18
|
+
* the observed spend, so replay never double-PAYS anything while the
|
|
19
|
+
* accounting stays one-directional. The envelope lives for one
|
|
20
|
+
* invocation and is never persisted.
|
|
21
|
+
*
|
|
22
|
+
* Accounting is integer micro-USD and conservative at the
|
|
23
|
+
* representation boundary (v1.17.0 review P1-4): the cap converts DOWN
|
|
24
|
+
* (floor), every debit converts UP (ceil), and a cap below one
|
|
25
|
+
* micro-USD is rejected outright, so for any admitted sequence the sum
|
|
26
|
+
* of the ORIGINAL ceilings can never exceed maxTotalUsd and no
|
|
27
|
+
* positive ceiling ever debits zero. Amounts that are integer
|
|
28
|
+
* micro-USD up to float noise stay exact, so 0.1 + 0.2 against a 0.3
|
|
29
|
+
* envelope is a fit, not a float rejection.
|
|
30
|
+
*/
|
|
31
|
+
const MICRO = 1e6;
|
|
32
|
+
/**
|
|
33
|
+
* Relative tolerance for float noise around an integer micro-USD
|
|
34
|
+
* amount: 0.3 * 1e6 is 299999.99999999994 and MUST count as 300000,
|
|
35
|
+
* while a genuinely sub-micro 0.4 must not.
|
|
36
|
+
*/
|
|
37
|
+
const MICRO_NOISE = 1e-6;
|
|
38
|
+
function microOf(usd, direction) {
|
|
39
|
+
const raw = usd * MICRO;
|
|
40
|
+
const nearest = Math.round(raw);
|
|
41
|
+
if (Math.abs(raw - nearest) <= MICRO_NOISE * Math.max(1, Math.abs(nearest))) return nearest;
|
|
42
|
+
return direction === "floor" ? Math.floor(raw) : Math.ceil(raw);
|
|
43
|
+
}
|
|
44
|
+
/** Thrown when authorizing a run's ceiling would exceed the envelope. */
|
|
45
|
+
var SweepBudgetError = class extends Error {
|
|
46
|
+
/** What was about to start, e.g. `eval target 'sweep-math'`. */
|
|
47
|
+
runLabel;
|
|
48
|
+
/** The per-run ceiling that did not fit. */
|
|
49
|
+
ceilingUsd;
|
|
50
|
+
/** Total already authorized before this refusal. */
|
|
51
|
+
authorizedUsd;
|
|
52
|
+
maxTotalUsd;
|
|
53
|
+
constructor(runLabel, ceilingUsd, authorizedUsd, maxTotalUsd) {
|
|
54
|
+
super(`sweep envelope exhausted: authorizing $${String(ceilingUsd)} for ${runLabel} would exceed maxTotalUsd $${String(maxTotalUsd)} ($${String(authorizedUsd)} already authorized); the run was refused before any provider call`);
|
|
55
|
+
this.name = "SweepBudgetError";
|
|
56
|
+
this.runLabel = runLabel;
|
|
57
|
+
this.ceilingUsd = ceilingUsd;
|
|
58
|
+
this.authorizedUsd = authorizedUsd;
|
|
59
|
+
this.maxTotalUsd = maxTotalUsd;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* One envelope bounds one whole sweep invocation: share the instance
|
|
64
|
+
* across the canary loop and runSweepMatrix so canary, target, and
|
|
65
|
+
* judge runs all draw from the same remainder.
|
|
66
|
+
*/
|
|
67
|
+
var SpendEnvelope = class {
|
|
68
|
+
maxTotalUsd;
|
|
69
|
+
maxMicroUsd;
|
|
70
|
+
authorizedMicroUsd = 0;
|
|
71
|
+
constructor(maxTotalUsd) {
|
|
72
|
+
if (!Number.isFinite(maxTotalUsd) || maxTotalUsd <= 0) throw new ConfigError(`SpendEnvelope maxTotalUsd must be a positive finite number, got ${String(maxTotalUsd)}`);
|
|
73
|
+
this.maxTotalUsd = maxTotalUsd;
|
|
74
|
+
this.maxMicroUsd = microOf(maxTotalUsd, "floor");
|
|
75
|
+
if (this.maxMicroUsd < 1) throw new ConfigError(`SpendEnvelope maxTotalUsd ${String(maxTotalUsd)} is below the 1 micro-USD accounting granularity (\$0.000001); such an envelope could never admit a run`);
|
|
76
|
+
}
|
|
77
|
+
/** Total authorized so far (debit-only; never decreases). */
|
|
78
|
+
get authorizedUsd() {
|
|
79
|
+
return this.authorizedMicroUsd / MICRO;
|
|
80
|
+
}
|
|
81
|
+
get remainingUsd() {
|
|
82
|
+
return Math.max(0, this.maxMicroUsd - this.authorizedMicroUsd) / MICRO;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Authorizes one run's immutable ceiling or throws SweepBudgetError.
|
|
86
|
+
* An unbounded run cannot be authorized: under an envelope every run
|
|
87
|
+
* MUST carry an explicit positive ceiling, otherwise the aggregate
|
|
88
|
+
* bound would be unaccountable.
|
|
89
|
+
*/
|
|
90
|
+
authorize(ceilingUsd, runLabel) {
|
|
91
|
+
if (ceilingUsd === void 0 || !Number.isFinite(ceilingUsd) || ceilingUsd <= 0) throw new ConfigError(`the spend envelope requires an explicit positive per-run ceiling for ${runLabel}; got ${String(ceilingUsd)} (an unbounded run under an aggregate envelope would be unaccountable)`);
|
|
92
|
+
const micro = Math.max(1, microOf(ceilingUsd, "ceil"));
|
|
93
|
+
if (this.authorizedMicroUsd + micro > this.maxMicroUsd) throw new SweepBudgetError(runLabel, ceilingUsd, this.authorizedUsd, this.maxTotalUsd);
|
|
94
|
+
this.authorizedMicroUsd += micro;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
//#endregion
|
|
3
98
|
//#region src/case.ts
|
|
4
99
|
/**
|
|
5
100
|
* @rulvar/evals (M9-T02): EvalCase, the grader contract, and the case and
|
|
@@ -16,11 +111,14 @@ import { createHash } from "node:crypto";
|
|
|
16
111
|
var EvalJudgeError = class extends Error {
|
|
17
112
|
judgeRun;
|
|
18
113
|
status;
|
|
19
|
-
|
|
114
|
+
/** What the failing judge run actually spent (honest cost accounting). */
|
|
115
|
+
costUsd;
|
|
116
|
+
constructor(judgeRun, status, detail, costUsd = 0) {
|
|
20
117
|
super(`eval judge run '${judgeRun}' settled '${status}'${detail === void 0 ? "" : `: ${detail}`}`);
|
|
21
118
|
this.name = "EvalJudgeError";
|
|
22
119
|
this.judgeRun = judgeRun;
|
|
23
120
|
this.status = status;
|
|
121
|
+
this.costUsd = costUsd;
|
|
24
122
|
}
|
|
25
123
|
};
|
|
26
124
|
/**
|
|
@@ -32,6 +130,7 @@ var EvalJudgeError = class extends Error {
|
|
|
32
130
|
async function runEvalCase(engine, evalCase, options = {}) {
|
|
33
131
|
const name = options.name ?? evalCase.workflow.name;
|
|
34
132
|
const timing = {};
|
|
133
|
+
options.envelope?.authorize(options.budgetUsd, `eval target '${name}'`);
|
|
35
134
|
const handle = engine.run(evalCase.workflow, evalCase.args, {
|
|
36
135
|
name: `eval:${name}`,
|
|
37
136
|
...options.budgetUsd === void 0 ? {} : { budgetUsd: options.budgetUsd }
|
|
@@ -53,24 +152,46 @@ async function runEvalCase(engine, evalCase, options = {}) {
|
|
|
53
152
|
async judge(spec) {
|
|
54
153
|
const ordinal = judgeOrdinal;
|
|
55
154
|
judgeOrdinal += 1;
|
|
155
|
+
options.envelope?.authorize(options.judgeBudgetUsd, `eval judge '${name}:${String(ordinal)}'`);
|
|
56
156
|
const judged = await runJudge(engine, `${name}:${ordinal}`, spec, options.judgeBudgetUsd);
|
|
57
157
|
judgeCostUsd += judged.costUsd;
|
|
58
158
|
return judged.output;
|
|
59
159
|
}
|
|
60
160
|
};
|
|
61
161
|
const verdicts = [];
|
|
62
|
-
|
|
162
|
+
let incomplete;
|
|
163
|
+
for (const grader of evalCase.graders) try {
|
|
164
|
+
verdicts.push(await grader.grade(context));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (error instanceof SweepBudgetError) {
|
|
167
|
+
incomplete = {
|
|
168
|
+
reason: "judge-refused",
|
|
169
|
+
detail: error.message
|
|
170
|
+
};
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (error instanceof EvalJudgeError && error.status === "exhausted") {
|
|
174
|
+
judgeCostUsd += error.costUsd;
|
|
175
|
+
incomplete = {
|
|
176
|
+
reason: "judge-exhausted",
|
|
177
|
+
detail: error.message
|
|
178
|
+
};
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
63
183
|
const latencyMs = timing.start !== void 0 && timing.end !== void 0 ? Math.max(0, Date.parse(timing.end) - Date.parse(timing.start)) : 0;
|
|
64
184
|
return {
|
|
65
185
|
name,
|
|
66
186
|
status: outcome.status,
|
|
67
|
-
passed: outcome.status === "ok" && verdicts.every((verdict) => verdict.passed),
|
|
187
|
+
passed: outcome.status === "ok" && incomplete === void 0 && verdicts.every((verdict) => verdict.passed),
|
|
68
188
|
verdicts,
|
|
69
189
|
costUsd: outcome.cost.totalUsd + judgeCostUsd,
|
|
70
190
|
judgeCostUsd,
|
|
71
191
|
latencyMs,
|
|
72
192
|
usage: outcome.usage,
|
|
73
|
-
...outcome.error === void 0 ? {} : { error: outcome.error }
|
|
193
|
+
...outcome.error === void 0 ? {} : { error: outcome.error },
|
|
194
|
+
...incomplete === void 0 ? {} : { incomplete }
|
|
74
195
|
};
|
|
75
196
|
}
|
|
76
197
|
async function runJudge(engine, judgeName, spec, budgetUsd) {
|
|
@@ -87,7 +208,7 @@ async function runJudge(engine, judgeName, spec, budgetUsd) {
|
|
|
87
208
|
name: workflowName,
|
|
88
209
|
...budgetUsd === void 0 ? {} : { budgetUsd }
|
|
89
210
|
}).result;
|
|
90
|
-
if (outcome.status !== "ok") throw new EvalJudgeError(workflowName, outcome.status, outcome.error?.message);
|
|
211
|
+
if (outcome.status !== "ok") throw new EvalJudgeError(workflowName, outcome.status, outcome.error?.message, outcome.cost.totalUsd);
|
|
91
212
|
return {
|
|
92
213
|
output: outcome.value ?? null,
|
|
93
214
|
costUsd: outcome.cost.totalUsd
|
|
@@ -101,22 +222,37 @@ async function runJudge(engine, judgeName, spec, budgetUsd) {
|
|
|
101
222
|
async function runEvalSuite(engine, cases, options = {}) {
|
|
102
223
|
const seen = /* @__PURE__ */ new Map();
|
|
103
224
|
const results = [];
|
|
225
|
+
let refusal;
|
|
104
226
|
for (const evalCase of cases) {
|
|
105
227
|
const base = evalCase.workflow.name;
|
|
106
228
|
const ordinal = seen.get(base) ?? 0;
|
|
107
229
|
seen.set(base, ordinal + 1);
|
|
108
230
|
const name = ordinal === 0 ? base : `${base}#${ordinal}`;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
231
|
+
try {
|
|
232
|
+
results.push(await runEvalCase(engine, evalCase, {
|
|
233
|
+
name,
|
|
234
|
+
...options.budgetUsd === void 0 ? {} : { budgetUsd: options.budgetUsd },
|
|
235
|
+
...options.judgeBudgetUsd === void 0 ? {} : { judgeBudgetUsd: options.judgeBudgetUsd },
|
|
236
|
+
...options.envelope === void 0 ? {} : { envelope: options.envelope }
|
|
237
|
+
}));
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (!(error instanceof SweepBudgetError)) throw error;
|
|
240
|
+
refusal = {
|
|
241
|
+
runLabel: error.runLabel,
|
|
242
|
+
atCase: name,
|
|
243
|
+
detail: error.message
|
|
244
|
+
};
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
114
247
|
}
|
|
115
248
|
return {
|
|
116
249
|
results,
|
|
117
250
|
passRate: results.length === 0 ? 0 : results.filter((r) => r.passed).length / results.length,
|
|
118
251
|
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
|
|
252
|
+
meanLatencyMs: results.length === 0 ? 0 : results.reduce((sum, r) => sum + r.latencyMs, 0) / results.length,
|
|
253
|
+
plannedN: cases.length,
|
|
254
|
+
completedN: results.length,
|
|
255
|
+
...refusal === void 0 ? {} : { refusal }
|
|
120
256
|
};
|
|
121
257
|
}
|
|
122
258
|
//#endregion
|
|
@@ -337,26 +473,66 @@ function normalizeCanaryOutput(output) {
|
|
|
337
473
|
return (typeof output === "string" ? output : JSON.stringify(output ?? null)).normalize("NFC").trim().replace(/\s+/gu, " ");
|
|
338
474
|
}
|
|
339
475
|
/**
|
|
340
|
-
* Runs the fixed probe set through the ordinary engine
|
|
341
|
-
*
|
|
342
|
-
*
|
|
476
|
+
* Runs the fixed probe set through the ordinary engine. Probes run
|
|
477
|
+
* sequentially in declaration order, one run per probe, so recordings
|
|
478
|
+
* replay deterministically. Each probe run carries the optional
|
|
479
|
+
* immutable ceiling (options.budgetUsd) and authorizes it against the
|
|
480
|
+
* optional envelope before starting; an envelope refusal records the
|
|
481
|
+
* probe as 'refused' and keeps walking instead of throwing away the
|
|
482
|
+
* completed probes. A non-ok or refused probe enters the fingerprint
|
|
483
|
+
* as `!status` and clears allOk: callers gate drift flipping on allOk,
|
|
484
|
+
* because a budget-starved or transiently failing probe fingerprints
|
|
485
|
+
* differently without the model having drifted.
|
|
343
486
|
*/
|
|
344
|
-
async function
|
|
487
|
+
async function runCanary(engine, probes, options = {}) {
|
|
345
488
|
const outputs = [];
|
|
489
|
+
const probeReports = [];
|
|
346
490
|
for (const [index, prompt] of probes.prompts.entries()) {
|
|
491
|
+
try {
|
|
492
|
+
options.envelope?.authorize(options.budgetUsd, `canary probe ${String(index)}`);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (!(error instanceof SweepBudgetError)) throw error;
|
|
495
|
+
probeReports.push({
|
|
496
|
+
prompt,
|
|
497
|
+
status: "refused"
|
|
498
|
+
});
|
|
499
|
+
outputs.push("!refused");
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
347
502
|
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;
|
|
503
|
+
const outcome = await engine.run(workflow, null, options.budgetUsd === void 0 ? {} : { budgetUsd: options.budgetUsd }).result;
|
|
504
|
+
probeReports.push({
|
|
505
|
+
prompt,
|
|
506
|
+
status: outcome.status
|
|
507
|
+
});
|
|
349
508
|
outputs.push(outcome.status === "ok" ? normalizeCanaryOutput(outcome.value) : `!${outcome.status}`);
|
|
350
509
|
}
|
|
351
510
|
const body = JSON.stringify([probes.prompts.length, outputs]);
|
|
352
|
-
return
|
|
511
|
+
return {
|
|
512
|
+
fingerprint: createHash("sha256").update(body, "utf8").digest("hex"),
|
|
513
|
+
allOk: probeReports.every((probe) => probe.status === "ok"),
|
|
514
|
+
probes: probeReports
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* The fingerprint alone (the pre-v1.16.2-review surface, kept
|
|
519
|
+
* compatible). Prefer runCanary: its allOk is the drift-flip gate.
|
|
520
|
+
*/
|
|
521
|
+
async function canaryFingerprint(engine, probes, options = {}) {
|
|
522
|
+
return (await runCanary(engine, probes, options)).fingerprint;
|
|
353
523
|
}
|
|
354
524
|
/**
|
|
355
525
|
* Flips the model's ACTIVE eval-measured claims to stale when their
|
|
356
526
|
* recorded canary fingerprint differs from the fresh one. Claims
|
|
357
527
|
* without a recorded fingerprint have no baseline and
|
|
358
528
|
* stay untouched (the documented no-probe posture); a second run is
|
|
359
|
-
* an idempotent noop. CAS-rebased like every maintenance commit
|
|
529
|
+
* an idempotent noop. CAS-rebased like every maintenance commit; the
|
|
530
|
+
* retries run no engine work and pay nothing.
|
|
531
|
+
*
|
|
532
|
+
* Only pass fingerprints from an allOk probe set (runCanary): a
|
|
533
|
+
* fingerprint containing a `!status` probe differs from any healthy
|
|
534
|
+
* baseline by construction, and flipping on it would blame the model
|
|
535
|
+
* for a budget ceiling or a transient provider failure.
|
|
360
536
|
*/
|
|
361
537
|
async function flipStaleOnCanaryDrift(store, model, freshFingerprint, options) {
|
|
362
538
|
const attempts = options?.attempts ?? 3;
|
|
@@ -549,6 +725,16 @@ function renderCheckpointReport(report) {
|
|
|
549
725
|
}
|
|
550
726
|
//#endregion
|
|
551
727
|
//#region src/sweeps.ts
|
|
728
|
+
/**
|
|
729
|
+
* Matrix sweeps (M11-T02). The deconfounder of the whole
|
|
730
|
+
* knowledge feature: a FIXED eval matrix (workflow x model x
|
|
731
|
+
* taskClass), independent of current routing, measured through the
|
|
732
|
+
* ordinary engine (journaled, budgeted, VCR-recordable), emitting
|
|
733
|
+
* eval-measured claims through the eval-committer identity.
|
|
734
|
+
*
|
|
735
|
+
* Sweep volume is never authorized by proposal volume: the model pool
|
|
736
|
+
* and the case list are EXPLICIT caller data (fixed pools only).
|
|
737
|
+
*/
|
|
552
738
|
const SWEEP_THRESHOLD_DEFAULTS = {
|
|
553
739
|
strength: .9,
|
|
554
740
|
weakness: .5
|
|
@@ -575,6 +761,7 @@ async function runSweepMatrix(pool, options) {
|
|
|
575
761
|
...SWEEP_THRESHOLD_DEFAULTS,
|
|
576
762
|
...options.thresholds
|
|
577
763
|
};
|
|
764
|
+
if (options.envelope !== void 0 && options.suite?.budgetUsd === void 0) throw new ConfigError("runSweepMatrix: an aggregate envelope requires suite.budgetUsd (the per-target ceiling); unbounded targets under an envelope would be unaccountable");
|
|
578
765
|
const byTaskClass = /* @__PURE__ */ new Map();
|
|
579
766
|
for (const entry of pool.cases) {
|
|
580
767
|
const bucket = byTaskClass.get(entry.taskClass) ?? [];
|
|
@@ -586,19 +773,34 @@ async function runSweepMatrix(pool, options) {
|
|
|
586
773
|
for (const member of pool.models) {
|
|
587
774
|
const engine = await options.engineFor(member);
|
|
588
775
|
for (const [taskClass, bucket] of byTaskClass) {
|
|
589
|
-
const suite = await runEvalSuite(engine, bucket.map((entry) => entry.case),
|
|
776
|
+
const suite = await runEvalSuite(engine, bucket.map((entry) => entry.case), {
|
|
777
|
+
...options.suite ?? {},
|
|
778
|
+
...options.envelope === void 0 ? {} : { envelope: options.envelope }
|
|
779
|
+
});
|
|
780
|
+
const exhaustedRuns = suite.results.filter((result) => result.status === "exhausted").length;
|
|
781
|
+
const judgeIncompleteRuns = suite.results.filter((result) => result.incomplete !== void 0).length;
|
|
782
|
+
const incompleteReason = suite.refusal !== void 0 ? "envelope-exhausted" : suite.results.find((result) => result.incomplete !== void 0)?.incomplete?.reason;
|
|
590
783
|
const cell = {
|
|
591
784
|
model: member.model,
|
|
592
785
|
...member.effort === void 0 ? {} : { effort: member.effort },
|
|
593
786
|
taskClass,
|
|
594
787
|
passRate: suite.passRate,
|
|
595
|
-
n: suite.
|
|
788
|
+
n: suite.completedN,
|
|
789
|
+
plannedN: suite.plannedN,
|
|
596
790
|
totalCostUsd: suite.totalCostUsd,
|
|
597
|
-
caseNames: suite.results.map((result) => result.name)
|
|
791
|
+
caseNames: suite.results.map((result) => result.name),
|
|
792
|
+
...exhaustedRuns === 0 ? {} : { exhaustedRuns },
|
|
793
|
+
...judgeIncompleteRuns === 0 ? {} : { judgeIncompleteRuns },
|
|
794
|
+
...suite.refusal === void 0 ? {} : {
|
|
795
|
+
envelopeExhausted: true,
|
|
796
|
+
refusedRunLabel: suite.refusal.runLabel
|
|
797
|
+
},
|
|
798
|
+
...incompleteReason === void 0 ? {} : { incompleteReason }
|
|
598
799
|
};
|
|
599
800
|
cells.push(cell);
|
|
600
801
|
const polarity = cell.passRate >= thresholds.strength ? "strength" : cell.passRate <= thresholds.weakness ? "weakness" : void 0;
|
|
601
|
-
|
|
802
|
+
const complete = cell.n === cell.plannedN && exhaustedRuns === 0 && judgeIncompleteRuns === 0 && suite.refusal === void 0;
|
|
803
|
+
if (polarity !== void 0 && cell.n > 0 && complete) {
|
|
602
804
|
const epoch = options.modelEpochFor?.(member);
|
|
603
805
|
claims.push({
|
|
604
806
|
id: claimIdOf(options.reportId, member, taskClass),
|
|
@@ -639,4 +841,4 @@ async function runSweepMatrix(pool, options) {
|
|
|
639
841
|
return report;
|
|
640
842
|
}
|
|
641
843
|
//#endregion
|
|
642
|
-
export { EvalJudgeError, JUDGE_VERDICT_SCHEMA, SWEEP_THRESHOLD_DEFAULTS, canaryFingerprint, commitEvalMeasured, evalMeasuredClaim, flipStaleOnCanaryDrift, goldenGrader, judgeGrader, normalizeCanaryOutput, renderCheckpointReport, rubricGrader, runEvalCase, runEvalMatrix, runEvalSuite, runSweepMatrix, runValueCheckpoint, rungRuleHolds };
|
|
844
|
+
export { EvalJudgeError, JUDGE_VERDICT_SCHEMA, SWEEP_THRESHOLD_DEFAULTS, SpendEnvelope, SweepBudgetError, canaryFingerprint, commitEvalMeasured, evalMeasuredClaim, flipStaleOnCanaryDrift, goldenGrader, judgeGrader, normalizeCanaryOutput, renderCheckpointReport, rubricGrader, runCanary, runEvalCase, runEvalMatrix, runEvalSuite, runSweepMatrix, runValueCheckpoint, rungRuleHolds };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/evals",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "Rulvar evals: eval cases, golden outputs, rubric and judge graders, matrix sweeps, canary fingerprint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rulvar/
|
|
26
|
-
"@rulvar/
|
|
25
|
+
"@rulvar/core": "1.18.0",
|
|
26
|
+
"@rulvar/testing": "1.18.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.20.0",
|