@rulvar/evals 1.123.0 → 1.124.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 +44 -1
- package/dist/index.js +348 -2
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -785,4 +785,47 @@ declare function runValueCheckpoint(checkpointPool: CheckpointPool, options: Run
|
|
|
785
785
|
/** The deterministic render for the M12 gate docs amendment. */
|
|
786
786
|
declare function renderCheckpointReport(report: CheckpointReport): string;
|
|
787
787
|
//#endregion
|
|
788
|
-
|
|
788
|
+
//#region src/fault-injection.d.ts
|
|
789
|
+
/** One machine-checkable observation of a driven branch. */
|
|
790
|
+
interface FaultScenarioObservation {
|
|
791
|
+
/** The documented typed observable was produced exactly. */
|
|
792
|
+
matched: boolean;
|
|
793
|
+
/** What was actually observed, quoting the typed surfaces. */
|
|
794
|
+
detail: string;
|
|
795
|
+
}
|
|
796
|
+
/** One artifact a scenario leaves, JSON or raw text. */
|
|
797
|
+
interface FaultScenarioArtifact {
|
|
798
|
+
name: string;
|
|
799
|
+
content: string;
|
|
800
|
+
}
|
|
801
|
+
interface FaultScenarioReport {
|
|
802
|
+
scenario: string;
|
|
803
|
+
/** The never-observed-live branch this scenario exists to drive. */
|
|
804
|
+
doctrine: string;
|
|
805
|
+
observation: FaultScenarioObservation;
|
|
806
|
+
artifacts: FaultScenarioArtifact[];
|
|
807
|
+
}
|
|
808
|
+
interface FaultInjectionReport {
|
|
809
|
+
scenarios: FaultScenarioReport[];
|
|
810
|
+
/** Every scenario matched its documented observable. */
|
|
811
|
+
allMatched: boolean;
|
|
812
|
+
/** The artifact files written, when `artifactsDir` was given. */
|
|
813
|
+
artifactFiles?: string[];
|
|
814
|
+
}
|
|
815
|
+
interface RunFaultInjectionOptions {
|
|
816
|
+
/** Write one `<scenario>.json` artifact bundle per scenario here. */
|
|
817
|
+
artifactsDir?: string;
|
|
818
|
+
/** Run only these scenarios; an unknown name is a typed ConfigError. */
|
|
819
|
+
only?: readonly string[];
|
|
820
|
+
}
|
|
821
|
+
/** The scenario names in run order. */
|
|
822
|
+
declare const FAULT_SCENARIO_NAMES: readonly string[];
|
|
823
|
+
/**
|
|
824
|
+
* Runs the fault-injection scenarios sequentially and reports each
|
|
825
|
+
* driven branch's observation; with `artifactsDir`, writes one
|
|
826
|
+
* `<scenario>.json` bundle per scenario (the observation plus every
|
|
827
|
+
* artifact), the experiment-grade trace a review can cite.
|
|
828
|
+
*/
|
|
829
|
+
declare function runFaultInjection(options?: RunFaultInjectionOptions): Promise<FaultInjectionReport>;
|
|
830
|
+
//#endregion
|
|
831
|
+
export { type BenchmarkFingerprint, type BenchmarkMetricExtractor, type BenchmarkPercentiles, type BenchmarkReport, type BenchmarkRunRecord, type BenchmarkSpec, type BenchmarkVerification, 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, FAULT_SCENARIO_NAMES, type FaultInjectionReport, type FaultScenarioArtifact, type FaultScenarioObservation, type FaultScenarioReport, 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 RunBenchmarkOptions, type RunCheckpointOptions, type RunEvalCaseOptions, type RunEvalSuiteOptions, type RunFaultInjectionOptions, type RunSweepOptions, SWEEP_THRESHOLD_DEFAULTS, SpendEnvelope, SweepBudgetError, type SweepCase, type SweepCellReport, type SweepModel, type SweepPool, type SweepReport, type SweepThresholds, agentTypeRuleHolds, canaryFingerprint, commitEvalMeasured, evalMeasuredClaim, flipStaleOnCanaryDrift, goldenGrader, judgeGrader, normalizeCanaryOutput, renderCheckpointReport, rubricGrader, runBenchmark, runCanary, runEvalCase, runEvalMatrix, runEvalSuite, runFaultInjection, runSweepMatrix, runValueCheckpoint, rungRuleHolds };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { ConfigError, KnowledgeCasError, claimExpiry, compileVerifiedLayer, defineWorkflow, hashRunOutput, lastRunSettle } from "@rulvar/core";
|
|
2
|
+
import { ConfigError, InMemoryStore, JsonlFileStore, KnowledgeCasError, claimExpiry, compileVerifiedLayer, createEngine, defineWorkflow, hashRunOutput, journalPricingSnapshot, lastRunSettle, memoryQuotaLimiter } from "@rulvar/core";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { appendFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { FAKE_MODEL_REF, FakeAdapter } from "@rulvar/testing";
|
|
4
8
|
//#region src/envelope.ts
|
|
5
9
|
/**
|
|
6
10
|
* The debit-only aggregate spend envelope (v1.16.2 review P1-2). A
|
|
@@ -1208,4 +1212,346 @@ async function runSweepMatrix(pool, options) {
|
|
|
1208
1212
|
return report;
|
|
1209
1213
|
}
|
|
1210
1214
|
//#endregion
|
|
1211
|
-
|
|
1215
|
+
//#region src/fault-injection.ts
|
|
1216
|
+
/**
|
|
1217
|
+
* The fault-injection kit (RV811): the comparison experiments left a
|
|
1218
|
+
* standing list of fail-closed branches never observed live, and a
|
|
1219
|
+
* branch nobody has ever driven is a claim, not a guarantee. Each
|
|
1220
|
+
* scenario here DELIBERATELY drives one such branch on the real engine
|
|
1221
|
+
* with scripted adapters (zero provider calls, zero keys), verifies the
|
|
1222
|
+
* documented typed observable, and leaves experiment-grade artifacts
|
|
1223
|
+
* (the outcome, the journal, the raw bytes where the fault is a byte
|
|
1224
|
+
* fault). Fail closed like everything else in this package: a scenario
|
|
1225
|
+
* whose branch stops producing its documented observable reports
|
|
1226
|
+
* `matched: false` and the whole report says so, instead of the list
|
|
1227
|
+
* quietly becoming untested again.
|
|
1228
|
+
*/
|
|
1229
|
+
const ROUTING = { routing: { loop: FAKE_MODEL_REF } };
|
|
1230
|
+
const echoWorkflow = defineWorkflow({ name: "fault-kit-echo" }, async (ctx) => {
|
|
1231
|
+
return await ctx.agent("one small step");
|
|
1232
|
+
});
|
|
1233
|
+
const twoStepWorkflow = defineWorkflow({ name: "fault-kit-two-step" }, async (ctx) => {
|
|
1234
|
+
return {
|
|
1235
|
+
first: await ctx.agent("first step"),
|
|
1236
|
+
second: await ctx.agent("second step")
|
|
1237
|
+
};
|
|
1238
|
+
});
|
|
1239
|
+
function jsonArtifact(name, value) {
|
|
1240
|
+
return {
|
|
1241
|
+
name,
|
|
1242
|
+
content: JSON.stringify(value, null, 2)
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function errorText(thrown) {
|
|
1246
|
+
return thrown instanceof Error ? `${thrown.name}: ${thrown.message}` : String(thrown);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* RV711: the opt-in in-flight exposure cap refuses a dispatch whose
|
|
1250
|
+
* worst-case estimate does not fit, BEFORE any provider call, with the
|
|
1251
|
+
* typed transient refusal (never a claimed ceiling crossing).
|
|
1252
|
+
*/
|
|
1253
|
+
const inFlightExposure = {
|
|
1254
|
+
name: "in-flight-exposure-refusal",
|
|
1255
|
+
doctrine: "maxInFlightExposureUsd refuses the dispatch typed before any provider call (BudgetExhaustedError, reason 'in-flight-exposure'), never a silent wait and never a claimed budget-ceiling crossing",
|
|
1256
|
+
async run() {
|
|
1257
|
+
const adapter = new FakeAdapter({
|
|
1258
|
+
agents: { "*": "never dispatched" },
|
|
1259
|
+
capsOverrides: { pricing: {
|
|
1260
|
+
inputUsdPerMTok: 3,
|
|
1261
|
+
outputUsdPerMTok: 15
|
|
1262
|
+
} }
|
|
1263
|
+
});
|
|
1264
|
+
const store = new InMemoryStore();
|
|
1265
|
+
const outcome = await createEngine({
|
|
1266
|
+
adapters: [adapter],
|
|
1267
|
+
stores: { journal: store },
|
|
1268
|
+
defaults: ROUTING
|
|
1269
|
+
}).run(echoWorkflow, void 0, {
|
|
1270
|
+
runId: "fault-exposure",
|
|
1271
|
+
maxInFlightExposureUsd: 1e-4
|
|
1272
|
+
}).result;
|
|
1273
|
+
const message = outcome.error?.message ?? "";
|
|
1274
|
+
return {
|
|
1275
|
+
observation: {
|
|
1276
|
+
matched: (outcome.status === "exhausted" || outcome.status === "error") && message.includes("in flight exposure cap reached") && message.includes("refused before any provider call"),
|
|
1277
|
+
detail: `run status '${outcome.status}'; ${message}`
|
|
1278
|
+
},
|
|
1279
|
+
artifacts: [jsonArtifact("outcome.json", {
|
|
1280
|
+
status: outcome.status,
|
|
1281
|
+
error: outcome.error
|
|
1282
|
+
}), jsonArtifact("journal.json", await store.load("fault-exposure"))]
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
/**
|
|
1287
|
+
* RV704: two rules with one canonical content key are refused at the
|
|
1288
|
+
* shared construction chokepoint, because memory buckets by index while
|
|
1289
|
+
* keyed stores bucket by key, and one configuration must never admit
|
|
1290
|
+
* differently per storage.
|
|
1291
|
+
*/
|
|
1292
|
+
const duplicateQuotaRule = {
|
|
1293
|
+
name: "duplicate-quota-rule",
|
|
1294
|
+
doctrine: "a duplicated quota rule is refused typed at construction (ConfigError naming both indexes and the shared rule key), never admitted differently per storage backend",
|
|
1295
|
+
run() {
|
|
1296
|
+
let detail = "memoryQuotaLimiter accepted the duplicated rule set";
|
|
1297
|
+
let matched = false;
|
|
1298
|
+
try {
|
|
1299
|
+
memoryQuotaLimiter([{
|
|
1300
|
+
provider: "fake",
|
|
1301
|
+
requestsPerMinute: 10
|
|
1302
|
+
}, {
|
|
1303
|
+
provider: "fake",
|
|
1304
|
+
requestsPerMinute: 10
|
|
1305
|
+
}]);
|
|
1306
|
+
} catch (thrown) {
|
|
1307
|
+
detail = errorText(thrown);
|
|
1308
|
+
matched = thrown instanceof ConfigError && detail.includes("duplicates") && detail.includes("delete the duplicate");
|
|
1309
|
+
}
|
|
1310
|
+
return Promise.resolve({
|
|
1311
|
+
observation: {
|
|
1312
|
+
matched,
|
|
1313
|
+
detail
|
|
1314
|
+
},
|
|
1315
|
+
artifacts: [jsonArtifact("refusal.json", { detail })]
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
/** Runs the echo workflow into a fresh JsonlFileStore dir and returns the pieces. */
|
|
1320
|
+
async function seededJsonlRun(runId) {
|
|
1321
|
+
const dir = mkdtempSync(join(tmpdir(), "rulvar-fault-jsonl-"));
|
|
1322
|
+
const store = new JsonlFileStore({ dir });
|
|
1323
|
+
const outcome = await createEngine({
|
|
1324
|
+
adapters: [new FakeAdapter({ agents: { "*": "stored answer" } })],
|
|
1325
|
+
stores: { journal: store },
|
|
1326
|
+
defaults: ROUTING
|
|
1327
|
+
}).run(echoWorkflow, void 0, { runId }).result;
|
|
1328
|
+
if (outcome.status !== "ok") throw new Error(`fault kit: the seeding run settled '${outcome.status}' instead of ok`);
|
|
1329
|
+
const entries = await store.load(runId);
|
|
1330
|
+
const file = readdirSync(dir).filter((name) => name.includes(runId) && name.endsWith(".jsonl")).map((name) => join(dir, name))[0];
|
|
1331
|
+
if (file === void 0) throw new Error(`fault kit: no journal file found for '${runId}' in ${dir}`);
|
|
1332
|
+
return {
|
|
1333
|
+
dir,
|
|
1334
|
+
file,
|
|
1335
|
+
entries
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
const SCENARIOS = [
|
|
1339
|
+
inFlightExposure,
|
|
1340
|
+
duplicateQuotaRule,
|
|
1341
|
+
{
|
|
1342
|
+
name: "torn-jsonl-tail",
|
|
1343
|
+
doctrine: "a torn trailing JSONL line (crash mid-append) is discarded on load, every whole record is salvaged, and the file is repaired so the tear never accumulates",
|
|
1344
|
+
async run() {
|
|
1345
|
+
const { dir, file, entries } = await seededJsonlRun("fault-torn");
|
|
1346
|
+
appendFileSync(file, "{\"seq\": 9999, \"kind\": \"agent\", \"torn\": tr", "utf8");
|
|
1347
|
+
const bytesBefore = readFileSync(file, "utf8");
|
|
1348
|
+
const loaded = await new JsonlFileStore({ dir }).load("fault-torn");
|
|
1349
|
+
const bytesAfter = readFileSync(file, "utf8");
|
|
1350
|
+
const everyLineParses = bytesAfter.split("\n").filter((line) => line !== "").every((line) => {
|
|
1351
|
+
try {
|
|
1352
|
+
JSON.parse(line);
|
|
1353
|
+
return true;
|
|
1354
|
+
} catch {
|
|
1355
|
+
return false;
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
return {
|
|
1359
|
+
observation: {
|
|
1360
|
+
matched: loaded.length === entries.length && everyLineParses && !bytesAfter.includes("\"torn\": tr"),
|
|
1361
|
+
detail: `salvaged ${String(loaded.length)} of ${String(entries.length)} whole entries; the torn fragment was discarded and the file repaired in place (every remaining line parses: ${String(everyLineParses)})`
|
|
1362
|
+
},
|
|
1363
|
+
artifacts: [
|
|
1364
|
+
{
|
|
1365
|
+
name: "journal-torn.jsonl",
|
|
1366
|
+
content: bytesBefore
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
name: "journal-repaired.jsonl",
|
|
1370
|
+
content: bytesAfter
|
|
1371
|
+
},
|
|
1372
|
+
jsonArtifact("counts.json", {
|
|
1373
|
+
before: entries.length,
|
|
1374
|
+
salvaged: loaded.length
|
|
1375
|
+
})
|
|
1376
|
+
]
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
name: "glued-jsonl-tail",
|
|
1382
|
+
doctrine: "whole JSONL records glued onto one trailing line are accepted data: the load salvages every glued record instead of discarding them as a torn fragment",
|
|
1383
|
+
async run() {
|
|
1384
|
+
const { dir, file, entries } = await seededJsonlRun("fault-glued");
|
|
1385
|
+
const lines = readFileSync(file, "utf8").split("\n").filter((line) => line !== "");
|
|
1386
|
+
writeFileSync(file, [...lines.slice(0, -2), `${lines[lines.length - 2] ?? ""}${lines[lines.length - 1] ?? ""}`].join("\n"), "utf8");
|
|
1387
|
+
const bytesBefore = readFileSync(file, "utf8");
|
|
1388
|
+
const loaded = await new JsonlFileStore({ dir }).load("fault-glued");
|
|
1389
|
+
return {
|
|
1390
|
+
observation: {
|
|
1391
|
+
matched: loaded.length === entries.length && lines.length === entries.length,
|
|
1392
|
+
detail: `glued tail: ${String(loaded.length)} of ${String(entries.length)} records salvaged, the two glued records included, none discarded`
|
|
1393
|
+
},
|
|
1394
|
+
artifacts: [{
|
|
1395
|
+
name: "journal-glued.jsonl",
|
|
1396
|
+
content: bytesBefore
|
|
1397
|
+
}, jsonArtifact("counts.json", {
|
|
1398
|
+
before: entries.length,
|
|
1399
|
+
salvaged: loaded.length
|
|
1400
|
+
})]
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
name: "crash-resume-settle-boundary",
|
|
1406
|
+
doctrine: "a journal cut immediately after an agent terminal entry (the settle-boundary crash window) resumes to ok: the settled agent replays with zero live calls and only the unsettled remainder re-runs",
|
|
1407
|
+
async run() {
|
|
1408
|
+
const liveAdapter = new FakeAdapter({ agents: { "*": (call) => `answer: ${call.prompt}` } });
|
|
1409
|
+
const storeA = new InMemoryStore();
|
|
1410
|
+
const first = await createEngine({
|
|
1411
|
+
adapters: [liveAdapter],
|
|
1412
|
+
stores: { journal: storeA },
|
|
1413
|
+
defaults: ROUTING
|
|
1414
|
+
}).run(twoStepWorkflow, void 0, { runId: "fault-boundary" }).result;
|
|
1415
|
+
if (first.status !== "ok") throw new Error(`fault kit: the seeding run settled '${first.status}' instead of ok`);
|
|
1416
|
+
const entries = await storeA.load("fault-boundary");
|
|
1417
|
+
const settleIndex = entries.findIndex((entry) => entry.kind === "agent" && entry.status === "ok");
|
|
1418
|
+
const cut = entries.slice(0, settleIndex + 1);
|
|
1419
|
+
const storeB = new InMemoryStore();
|
|
1420
|
+
for (const entry of cut) await storeB.append("fault-boundary", entry);
|
|
1421
|
+
let liveCalls = 0;
|
|
1422
|
+
const resumed = await createEngine({
|
|
1423
|
+
adapters: [new FakeAdapter({ agents: { "*": (call) => {
|
|
1424
|
+
liveCalls += 1;
|
|
1425
|
+
return `answer: ${call.prompt}`;
|
|
1426
|
+
} } })],
|
|
1427
|
+
stores: { journal: storeB },
|
|
1428
|
+
defaults: ROUTING
|
|
1429
|
+
}).resume("fault-boundary", twoStepWorkflow).result;
|
|
1430
|
+
return {
|
|
1431
|
+
observation: {
|
|
1432
|
+
matched: resumed.status === "ok" && liveCalls === 1,
|
|
1433
|
+
detail: `resumed '${resumed.status}' from the post-settle crash window (journal cut at seq ${String(cut[cut.length - 1]?.seq ?? -1)} of ${String(entries.length)} entries): liveCalls=${String(liveCalls)}, the settled step replayed free`
|
|
1434
|
+
},
|
|
1435
|
+
artifacts: [jsonArtifact("journal-cut.json", cut), jsonArtifact("resume-outcome.json", {
|
|
1436
|
+
status: resumed.status,
|
|
1437
|
+
liveCalls,
|
|
1438
|
+
value: resumed.value ?? null
|
|
1439
|
+
})]
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
name: "pricing-rotation-uncovered-tail",
|
|
1445
|
+
doctrine: "after a price-table rotation that drops the model, the pinned segment still prices under its own pin while the uncovered tail folds unpriced (undefined), never silently at the stale pinned rates",
|
|
1446
|
+
async run() {
|
|
1447
|
+
const adapter = new FakeAdapter({ agents: { "*": "priced answer" } });
|
|
1448
|
+
const store = new InMemoryStore();
|
|
1449
|
+
const outcome = await createEngine({
|
|
1450
|
+
adapters: [adapter],
|
|
1451
|
+
stores: { journal: store },
|
|
1452
|
+
defaults: ROUTING,
|
|
1453
|
+
pricing: {
|
|
1454
|
+
pricingVersion: "fault-v1",
|
|
1455
|
+
models: { [FAKE_MODEL_REF]: {
|
|
1456
|
+
inputUsdPerMTok: 3,
|
|
1457
|
+
outputUsdPerMTok: 15
|
|
1458
|
+
} }
|
|
1459
|
+
}
|
|
1460
|
+
}).run(echoWorkflow, void 0, { runId: "fault-rotation" }).result;
|
|
1461
|
+
if (outcome.status !== "ok") throw new Error(`fault kit: the seeding run settled '${outcome.status}' instead of ok`);
|
|
1462
|
+
const snapshot = journalPricingSnapshot(await store.load("fault-rotation"));
|
|
1463
|
+
const composed = snapshot?.composedPriceUsd(() => void 0);
|
|
1464
|
+
const usage = {
|
|
1465
|
+
inputTokens: 1e3,
|
|
1466
|
+
outputTokens: 100,
|
|
1467
|
+
cacheReadTokens: 0,
|
|
1468
|
+
cacheWriteTokens: 0
|
|
1469
|
+
};
|
|
1470
|
+
const pinnedSegmentUsd = composed?.(FAKE_MODEL_REF, usage, 1);
|
|
1471
|
+
const uncoveredTailUsd = composed?.(FAKE_MODEL_REF, usage, (snapshot?.pinnedThroughSeq ?? 0) + 1);
|
|
1472
|
+
const ghostUsd = composed?.("ghost:model", usage, 1);
|
|
1473
|
+
return {
|
|
1474
|
+
observation: {
|
|
1475
|
+
matched: typeof pinnedSegmentUsd === "number" && pinnedSegmentUsd > 0 && uncoveredTailUsd === void 0 && ghostUsd === void 0,
|
|
1476
|
+
detail: `pinned segment priced under '${snapshot?.pricingVersion ?? "none"}' (${String(pinnedSegmentUsd)} USD); the rotated tail and the ghost model fold unpriced (undefined), surfaced, never a silent zero`
|
|
1477
|
+
},
|
|
1478
|
+
artifacts: [jsonArtifact("pricing-snapshot.json", {
|
|
1479
|
+
pricingVersion: snapshot?.pricingVersion,
|
|
1480
|
+
pinnedThroughSeq: snapshot?.pinnedThroughSeq,
|
|
1481
|
+
segments: snapshot?.segments
|
|
1482
|
+
}), jsonArtifact("fold.json", {
|
|
1483
|
+
pinnedSegmentUsd: pinnedSegmentUsd ?? null,
|
|
1484
|
+
uncoveredTailUsd: uncoveredTailUsd ?? null,
|
|
1485
|
+
ghostUsd: ghostUsd ?? null
|
|
1486
|
+
})]
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
name: "unknown-provider-id",
|
|
1492
|
+
doctrine: "routing to an unregistered provider id fails typed naming the id; nothing dispatches and nothing settles ok",
|
|
1493
|
+
async run() {
|
|
1494
|
+
const adapter = new FakeAdapter({ agents: { "*": "never used" } });
|
|
1495
|
+
const store = new InMemoryStore();
|
|
1496
|
+
let detail = "";
|
|
1497
|
+
let matched = false;
|
|
1498
|
+
try {
|
|
1499
|
+
const outcome = await createEngine({
|
|
1500
|
+
adapters: [adapter],
|
|
1501
|
+
stores: { journal: store },
|
|
1502
|
+
defaults: { routing: { loop: "ghost:model" } }
|
|
1503
|
+
}).run(echoWorkflow, void 0, { runId: "fault-ghost" }).result;
|
|
1504
|
+
detail = `run status '${outcome.status}': ${outcome.error?.message ?? ""}`;
|
|
1505
|
+
matched = outcome.status === "error" && (outcome.error?.message ?? "").includes("ghost");
|
|
1506
|
+
} catch (thrown) {
|
|
1507
|
+
detail = errorText(thrown);
|
|
1508
|
+
matched = detail.includes("ghost");
|
|
1509
|
+
}
|
|
1510
|
+
return {
|
|
1511
|
+
observation: {
|
|
1512
|
+
matched,
|
|
1513
|
+
detail
|
|
1514
|
+
},
|
|
1515
|
+
artifacts: [jsonArtifact("refusal.json", { detail })]
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
];
|
|
1520
|
+
/** The scenario names in run order. */
|
|
1521
|
+
const FAULT_SCENARIO_NAMES = SCENARIOS.map((scenario) => scenario.name);
|
|
1522
|
+
/**
|
|
1523
|
+
* Runs the fault-injection scenarios sequentially and reports each
|
|
1524
|
+
* driven branch's observation; with `artifactsDir`, writes one
|
|
1525
|
+
* `<scenario>.json` bundle per scenario (the observation plus every
|
|
1526
|
+
* artifact), the experiment-grade trace a review can cite.
|
|
1527
|
+
*/
|
|
1528
|
+
async function runFaultInjection(options) {
|
|
1529
|
+
const known = new Map(SCENARIOS.map((scenario) => [scenario.name, scenario]));
|
|
1530
|
+
for (const name of options?.only ?? []) if (!known.has(name)) throw new ConfigError(`unknown fault scenario '${name}'; known: ${FAULT_SCENARIO_NAMES.join(", ")}`);
|
|
1531
|
+
const selected = options?.only === void 0 ? SCENARIOS : SCENARIOS.filter((scenario) => options.only?.includes(scenario.name));
|
|
1532
|
+
const scenarios = [];
|
|
1533
|
+
const artifactFiles = [];
|
|
1534
|
+
for (const scenario of selected) {
|
|
1535
|
+
const { observation, artifacts } = await scenario.run();
|
|
1536
|
+
const report = {
|
|
1537
|
+
scenario: scenario.name,
|
|
1538
|
+
doctrine: scenario.doctrine,
|
|
1539
|
+
observation,
|
|
1540
|
+
artifacts
|
|
1541
|
+
};
|
|
1542
|
+
scenarios.push(report);
|
|
1543
|
+
if (options?.artifactsDir !== void 0) {
|
|
1544
|
+
mkdirSync(options.artifactsDir, { recursive: true });
|
|
1545
|
+
const file = join(options.artifactsDir, `${scenario.name}.json`);
|
|
1546
|
+
writeFileSync(file, JSON.stringify(report, null, 2), "utf8");
|
|
1547
|
+
artifactFiles.push(file);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return {
|
|
1551
|
+
scenarios,
|
|
1552
|
+
allMatched: scenarios.every((report) => report.observation.matched),
|
|
1553
|
+
...options?.artifactsDir === void 0 ? {} : { artifactFiles }
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
//#endregion
|
|
1557
|
+
export { EvalJudgeError, FAULT_SCENARIO_NAMES, JUDGE_VERDICT_SCHEMA, SWEEP_THRESHOLD_DEFAULTS, SpendEnvelope, SweepBudgetError, agentTypeRuleHolds, canaryFingerprint, commitEvalMeasured, evalMeasuredClaim, flipStaleOnCanaryDrift, goldenGrader, judgeGrader, normalizeCanaryOutput, renderCheckpointReport, rubricGrader, runBenchmark, runCanary, runEvalCase, runEvalMatrix, runEvalSuite, runFaultInjection, runSweepMatrix, runValueCheckpoint, rungRuleHolds };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/evals",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.124.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/core": "1.
|
|
26
|
-
"@rulvar/testing": "1.
|
|
25
|
+
"@rulvar/core": "1.124.0",
|
|
26
|
+
"@rulvar/testing": "1.124.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.20.1",
|