@tangle-network/agent-eval 0.89.0 → 0.90.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/dist/chunk-STGVSCDH.js +202 -0
- package/dist/chunk-STGVSCDH.js.map +1 -0
- package/dist/contract/index.js +9 -9
- package/dist/fuzz.d.ts +29 -0
- package/dist/fuzz.js +67 -37
- package/dist/fuzz.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/openapi.json +1 -1
- package/dist/perf/index.d.ts +123 -0
- package/dist/perf/index.js +18 -0
- package/dist/perf/index.js.map +1 -0
- package/dist/rl.js +6 -6
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,15 @@ All notable changes to `@tangle-network/agent-eval` and its sibling `agent-eval-
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## [0.
|
|
7
|
+
## [0.90.0] — 2026-06-10 — infra perf-benchmark substrate (`/perf`)
|
|
8
|
+
|
|
9
|
+
Domain-agnostic infra-performance benchmarking: a journeys × axes scenario matrix, record-integrity contracts over flat metric records, and a percentile ratchet. Complements the judge-panel `BenchmarkRunner` (root) — that one scores QUALITY via judges; `/perf` scores LATENCY / RELIABILITY. All additive — no existing export changed.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`JourneySpec` + `expandMatrix` + `scenarioKey` (`/perf` + root).** A journey is one measurable user path (`provision.cold`, `chat.ttft`) carrying its own data contract: `requiredFields` (must be non-null on a passing record), `minimums` (numeric floors, e.g. `event_count ≥ 1` for streaming), `phaseFields` (per-phase breakdown, reported separately), and `requiresLLM` (nightly vs per-PR scheduling). `expandMatrix` does the cartesian expansion over free-form `ScenarioAxes` (driver × region × …) with a `filter` for invalid combos; scenario keys are `journeyId|dim=value|…` with dims sorted, so the key is stable across axes-object insertion order.
|
|
14
|
+
- **`checkRecordIntegrity` + `assertRecordIntegrity` (`/perf` + root).** A record claiming `pass === true` must actually carry its journey's required measurements — a "passing" run with a null `total_ms` is an integrity violation (`null-required-field` / `below-minimum`), not a pass. Failed records are exempt (an errored run legitimately has nulls); `resolveJourney` returning null skips the record. The assert variant throws listing every violation.
|
|
15
|
+
- **`summarizeRecords` + `gatePerf` (`/perf` + root).** Percentile ratchet: fold flat records into per-scenario `PerfStat` (`p50` / `p90` / `n`, nearest-rank on sorted values), then gate a current `PerfBaseline` against a committed one. Null / non-numeric metric values are excluded from `n` and a zero-sample field is omitted — no fake zeros. Regressions trip when p50 OR p90 exceed `tolerancePct` (default 10) over baseline; strict improvements are reported with negative `overBy`; scenarios under `minSamples` (default 3) in current are surfaced in `missingScenarios` and never gated; baseline/current key drift lands in `missingScenarios` / `newScenarios`.
|
|
8
16
|
|
|
9
17
|
One clean, canonical version of five generic patterns the fleet kept hand-rolling across 2–4 product agents each. All additive — no existing export changed.
|
|
10
18
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// src/perf/integrity.ts
|
|
2
|
+
function isMissing(value) {
|
|
3
|
+
return value === null || value === void 0;
|
|
4
|
+
}
|
|
5
|
+
function checkRecordIntegrity(records, resolveJourney) {
|
|
6
|
+
const violations = [];
|
|
7
|
+
for (const [recordIndex, record] of records.entries()) {
|
|
8
|
+
if (record.pass !== true) continue;
|
|
9
|
+
const journey = resolveJourney(record);
|
|
10
|
+
if (journey === null) continue;
|
|
11
|
+
for (const field of journey.requiredFields) {
|
|
12
|
+
if (isMissing(record[field])) {
|
|
13
|
+
violations.push({
|
|
14
|
+
recordIndex,
|
|
15
|
+
journeyId: journey.id,
|
|
16
|
+
field,
|
|
17
|
+
reason: "null-required-field",
|
|
18
|
+
detail: `required field '${field}' is ${record[field] === null ? "null" : "undefined"} on a passing '${journey.id}' record`
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const field of journey.phaseFields ?? []) {
|
|
23
|
+
if (isMissing(record[field])) {
|
|
24
|
+
violations.push({
|
|
25
|
+
recordIndex,
|
|
26
|
+
journeyId: journey.id,
|
|
27
|
+
field,
|
|
28
|
+
reason: "null-required-field",
|
|
29
|
+
detail: `phase field '${field}' is ${record[field] === null ? "null" : "undefined"} on a passing '${journey.id}' record`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const { field, min } of journey.minimums ?? []) {
|
|
34
|
+
const value = record[field];
|
|
35
|
+
if (isMissing(value)) continue;
|
|
36
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
37
|
+
violations.push({
|
|
38
|
+
recordIndex,
|
|
39
|
+
journeyId: journey.id,
|
|
40
|
+
field,
|
|
41
|
+
reason: "below-minimum",
|
|
42
|
+
detail: `field '${field}' has non-numeric value ${JSON.stringify(value)} on a passing '${journey.id}' record (minimum ${min})`
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (value < min) {
|
|
47
|
+
violations.push({
|
|
48
|
+
recordIndex,
|
|
49
|
+
journeyId: journey.id,
|
|
50
|
+
field,
|
|
51
|
+
reason: "below-minimum",
|
|
52
|
+
detail: `field '${field}' is ${value}, below minimum ${min} on a passing '${journey.id}' record`
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { succeeded: violations.length === 0, violations };
|
|
58
|
+
}
|
|
59
|
+
function assertRecordIntegrity(records, resolveJourney) {
|
|
60
|
+
const result = checkRecordIntegrity(records, resolveJourney);
|
|
61
|
+
if (result.succeeded) return;
|
|
62
|
+
const lines = result.violations.map(
|
|
63
|
+
(v) => ` [record ${v.recordIndex}] ${v.journeyId}.${v.field} (${v.reason}): ${v.detail}`
|
|
64
|
+
);
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Record integrity check failed with ${result.violations.length} violation(s):
|
|
67
|
+
${lines.join("\n")}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/perf/journey.ts
|
|
72
|
+
function scenarioKey(journeyId, axes) {
|
|
73
|
+
const parts = Object.keys(axes).sort().map((dim) => `${dim}=${axes[dim]}`);
|
|
74
|
+
return [journeyId, ...parts].join("|");
|
|
75
|
+
}
|
|
76
|
+
function expandMatrix(journeys, axes, filter) {
|
|
77
|
+
const dims = Object.keys(axes).sort();
|
|
78
|
+
let combos = [{}];
|
|
79
|
+
for (const dim of dims) {
|
|
80
|
+
const values = axes[dim];
|
|
81
|
+
const next = [];
|
|
82
|
+
for (const combo of combos) {
|
|
83
|
+
for (const value of values) {
|
|
84
|
+
next.push({ ...combo, [dim]: value });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
combos = next;
|
|
88
|
+
}
|
|
89
|
+
const scenarios = [];
|
|
90
|
+
for (const journey of journeys) {
|
|
91
|
+
for (const combo of combos) {
|
|
92
|
+
if (filter && !filter(journey.id, combo)) continue;
|
|
93
|
+
scenarios.push({ key: scenarioKey(journey.id, combo), journey, axes: combo });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return scenarios;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/perf/ratchet.ts
|
|
100
|
+
function nearestRank(sorted, p) {
|
|
101
|
+
if (sorted.length === 0) throw new Error("nearestRank requires at least one sample");
|
|
102
|
+
const rank = Math.max(1, Math.ceil(p / 100 * sorted.length));
|
|
103
|
+
return sorted[rank - 1];
|
|
104
|
+
}
|
|
105
|
+
function summarizeRecords(records, keyOf, metricFields) {
|
|
106
|
+
const samples = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const record of records) {
|
|
108
|
+
const key = keyOf(record);
|
|
109
|
+
if (key === null) continue;
|
|
110
|
+
let byField = samples.get(key);
|
|
111
|
+
if (!byField) {
|
|
112
|
+
byField = /* @__PURE__ */ new Map();
|
|
113
|
+
samples.set(key, byField);
|
|
114
|
+
}
|
|
115
|
+
for (const field of metricFields) {
|
|
116
|
+
const value = record[field];
|
|
117
|
+
if (typeof value !== "number" || Number.isNaN(value)) continue;
|
|
118
|
+
let values = byField.get(field);
|
|
119
|
+
if (!values) {
|
|
120
|
+
values = [];
|
|
121
|
+
byField.set(field, values);
|
|
122
|
+
}
|
|
123
|
+
values.push(value);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const scenarios = {};
|
|
127
|
+
for (const [key, byField] of samples) {
|
|
128
|
+
const stats = {};
|
|
129
|
+
for (const [field, values] of byField) {
|
|
130
|
+
if (values.length === 0) continue;
|
|
131
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
132
|
+
stats[field] = {
|
|
133
|
+
p50: nearestRank(sorted, 50),
|
|
134
|
+
p90: nearestRank(sorted, 90),
|
|
135
|
+
n: sorted.length
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
scenarios[key] = stats;
|
|
139
|
+
}
|
|
140
|
+
return { version: 1, scenarios };
|
|
141
|
+
}
|
|
142
|
+
function pctOver(currentValue, baselineValue) {
|
|
143
|
+
if (baselineValue === 0) {
|
|
144
|
+
return currentValue === 0 ? 0 : Number.POSITIVE_INFINITY;
|
|
145
|
+
}
|
|
146
|
+
return (currentValue - baselineValue) / baselineValue * 100;
|
|
147
|
+
}
|
|
148
|
+
function gatePerf(current, baseline, options) {
|
|
149
|
+
const tolerancePct = options?.tolerancePct ?? 10;
|
|
150
|
+
const minSamples = options?.minSamples ?? 3;
|
|
151
|
+
const regressions = [];
|
|
152
|
+
const improvements = [];
|
|
153
|
+
const missing = /* @__PURE__ */ new Set();
|
|
154
|
+
for (const [scenarioKey2, baselineStats] of Object.entries(baseline.scenarios)) {
|
|
155
|
+
const currentStats = current.scenarios[scenarioKey2];
|
|
156
|
+
if (!currentStats) {
|
|
157
|
+
missing.add(scenarioKey2);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const [field, baselineStat] of Object.entries(baselineStats)) {
|
|
161
|
+
const currentStat = currentStats[field];
|
|
162
|
+
if (!currentStat || currentStat.n < minSamples) {
|
|
163
|
+
missing.add(scenarioKey2);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const overBy = {
|
|
167
|
+
p50Pct: pctOver(currentStat.p50, baselineStat.p50),
|
|
168
|
+
p90Pct: pctOver(currentStat.p90, baselineStat.p90)
|
|
169
|
+
};
|
|
170
|
+
const entry = {
|
|
171
|
+
scenarioKey: scenarioKey2,
|
|
172
|
+
field,
|
|
173
|
+
baseline: baselineStat,
|
|
174
|
+
current: currentStat,
|
|
175
|
+
overBy
|
|
176
|
+
};
|
|
177
|
+
if (overBy.p50Pct > tolerancePct || overBy.p90Pct > tolerancePct) {
|
|
178
|
+
regressions.push(entry);
|
|
179
|
+
} else if (overBy.p50Pct < 0 && overBy.p90Pct < 0) {
|
|
180
|
+
improvements.push(entry);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const newScenarios = Object.keys(current.scenarios).filter((key) => !(key in baseline.scenarios));
|
|
185
|
+
return {
|
|
186
|
+
succeeded: regressions.length === 0,
|
|
187
|
+
regressions,
|
|
188
|
+
improvements,
|
|
189
|
+
missingScenarios: [...missing],
|
|
190
|
+
newScenarios
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export {
|
|
195
|
+
checkRecordIntegrity,
|
|
196
|
+
assertRecordIntegrity,
|
|
197
|
+
scenarioKey,
|
|
198
|
+
expandMatrix,
|
|
199
|
+
summarizeRecords,
|
|
200
|
+
gatePerf
|
|
201
|
+
};
|
|
202
|
+
//# sourceMappingURL=chunk-STGVSCDH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/perf/integrity.ts","../src/perf/journey.ts","../src/perf/ratchet.ts"],"sourcesContent":["/**\n * Record-integrity contracts for perf metric records.\n *\n * A record that claims `pass === true` must actually carry the journey's\n * required measurements — a \"passing\" provision run with a null\n * `total_ms` is a lying record, not a pass. Failed records are exempt:\n * a run that errored mid-flight legitimately has nulls.\n */\n\nimport type { JourneySpec } from './journey'\n\nexport interface IntegrityViolation {\n recordIndex: number\n journeyId: string\n field: string\n reason: 'null-required-field' | 'below-minimum'\n detail: string\n}\n\nexport interface IntegrityResult {\n succeeded: boolean\n violations: IntegrityViolation[]\n}\n\nfunction isMissing(value: unknown): boolean {\n return value === null || value === undefined\n}\n\n/**\n * Validates flat metric records (Record<string, unknown> with a boolean\n * `pass` field) against their journey contract. Only records with\n * pass === true are checked — a failed record may legitimately have nulls.\n * resolveJourney maps a record to its JourneySpec (or null to skip).\n */\nexport function checkRecordIntegrity(\n records: ReadonlyArray<Record<string, unknown>>,\n resolveJourney: (record: Record<string, unknown>) => JourneySpec | null,\n): IntegrityResult {\n const violations: IntegrityViolation[] = []\n for (const [recordIndex, record] of records.entries()) {\n if (record.pass !== true) continue\n const journey = resolveJourney(record)\n if (journey === null) continue\n for (const field of journey.requiredFields) {\n if (isMissing(record[field])) {\n violations.push({\n recordIndex,\n journeyId: journey.id,\n field,\n reason: 'null-required-field',\n detail: `required field '${field}' is ${record[field] === null ? 'null' : 'undefined'} on a passing '${journey.id}' record`,\n })\n }\n }\n for (const field of journey.phaseFields ?? []) {\n if (isMissing(record[field])) {\n violations.push({\n recordIndex,\n journeyId: journey.id,\n field,\n reason: 'null-required-field',\n detail: `phase field '${field}' is ${record[field] === null ? 'null' : 'undefined'} on a passing '${journey.id}' record`,\n })\n }\n }\n for (const { field, min } of journey.minimums ?? []) {\n const value = record[field]\n if (isMissing(value)) continue // null-ness is the required/phase fields' contract\n if (typeof value !== 'number' || Number.isNaN(value)) {\n violations.push({\n recordIndex,\n journeyId: journey.id,\n field,\n reason: 'below-minimum',\n detail: `field '${field}' has non-numeric value ${JSON.stringify(value)} on a passing '${journey.id}' record (minimum ${min})`,\n })\n continue\n }\n if (value < min) {\n violations.push({\n recordIndex,\n journeyId: journey.id,\n field,\n reason: 'below-minimum',\n detail: `field '${field}' is ${value}, below minimum ${min} on a passing '${journey.id}' record`,\n })\n }\n }\n }\n return { succeeded: violations.length === 0, violations }\n}\n\n/** Throws an Error listing every violation when the result fails. */\nexport function assertRecordIntegrity(\n records: ReadonlyArray<Record<string, unknown>>,\n resolveJourney: (record: Record<string, unknown>) => JourneySpec | null,\n): void {\n const result = checkRecordIntegrity(records, resolveJourney)\n if (result.succeeded) return\n const lines = result.violations.map(\n (v) => ` [record ${v.recordIndex}] ${v.journeyId}.${v.field} (${v.reason}): ${v.detail}`,\n )\n throw new Error(\n `Record integrity check failed with ${result.violations.length} violation(s):\\n${lines.join('\\n')}`,\n )\n}\n","/**\n * Journey × axes matrix for infra performance benchmarks.\n *\n * A journey is one measurable user path (\"provision.cold\", \"chat.ttft\");\n * axes are free-form scenario dimensions (driver, region, image…). The\n * matrix expansion is pure bookkeeping — running the scenarios and\n * recording metrics is the caller's job. This module complements the\n * judge-panel `BenchmarkRunner` (src/benchmark.ts): that one scores\n * QUALITY via judges, this one structures LATENCY / RELIABILITY runs\n * over flat metric records.\n */\n\n/** One measurable user journey (e.g. \"provision.cold\", \"chat.ttft\"). */\nexport interface JourneySpec {\n id: string\n description: string\n /** Needs a real LLM call — schedule nightly, not per-PR. */\n requiresLLM: boolean\n /**\n * Fields that MUST be non-null on a passing record of this journey.\n * A \"passing\" record missing one is an integrity violation, not a pass.\n */\n requiredFields: ReadonlyArray<string>\n /** Numeric floors, e.g. {field: 'event_count', min: 1} for streaming. */\n minimums?: ReadonlyArray<{ field: string; min: number }>\n /** Per-phase breakdown fields expected non-null (subset of requiredFields semantics, reported separately). */\n phaseFields?: ReadonlyArray<string>\n}\n\nexport interface ScenarioAxes {\n /** e.g. driver: ['docker','firecracker'] — every key is a free-form dimension. */\n [dimension: string]: ReadonlyArray<string>\n}\n\nexport interface PerfScenario {\n /** `${journeyId}|${dim1}=${v1}|${dim2}=${v2}` (dims sorted). */\n key: string\n journey: JourneySpec\n axes: Record<string, string>\n}\n\n/** Stable scenario key: journey id then `dim=value` pairs in sorted-dim order. */\nexport function scenarioKey(journeyId: string, axes: Record<string, string>): string {\n const parts = Object.keys(axes)\n .sort()\n .map((dim) => `${dim}=${axes[dim]}`)\n return [journeyId, ...parts].join('|')\n}\n\n/** Cartesian expansion; `filter` lets callers drop invalid combos (e.g. firecracker×resume). */\nexport function expandMatrix(\n journeys: ReadonlyArray<JourneySpec>,\n axes: ScenarioAxes,\n filter?: (journeyId: string, combo: Record<string, string>) => boolean,\n): PerfScenario[] {\n const dims = Object.keys(axes).sort()\n let combos: Record<string, string>[] = [{}]\n for (const dim of dims) {\n const values = axes[dim] as ReadonlyArray<string>\n const next: Record<string, string>[] = []\n for (const combo of combos) {\n for (const value of values) {\n next.push({ ...combo, [dim]: value })\n }\n }\n combos = next\n }\n const scenarios: PerfScenario[] = []\n for (const journey of journeys) {\n for (const combo of combos) {\n if (filter && !filter(journey.id, combo)) continue\n scenarios.push({ key: scenarioKey(journey.id, combo), journey, axes: combo })\n }\n }\n return scenarios\n}\n","/**\n * Percentile ratchet over perf metric records.\n *\n * `summarizeRecords` folds flat records into per-scenario p50/p90 stats;\n * `gatePerf` compares a current summary against a committed baseline and\n * trips on regressions beyond tolerance. Percentiles use nearest-rank on\n * sorted values. Null / non-numeric metric values are excluded from the\n * stat (n reflects only real samples); a field with zero samples is\n * omitted entirely — no fake zeros.\n */\n\nexport interface PerfStat {\n p50: number\n p90: number\n n: number\n}\n\nexport interface PerfBaseline {\n version: 1\n /** key → metric field → stat. */\n scenarios: Record<string, Record<string, PerfStat>>\n}\n\nexport interface PerfRegression {\n scenarioKey: string\n field: string\n baseline: PerfStat\n current: PerfStat\n /** percent over baseline p50 / p90, whichever tripped. */\n overBy: { p50Pct: number; p90Pct: number }\n}\n\nexport interface PerfGateResult {\n succeeded: boolean\n regressions: PerfRegression[]\n /** Negative overBy: strictly better than baseline on both percentiles. */\n improvements: PerfRegression[]\n /** In baseline but absent (or under-sampled, n < minSamples) in current. */\n missingScenarios: string[]\n /** In records but absent from baseline. */\n newScenarios: string[]\n}\n\n/** Nearest-rank percentile over a non-empty ascending-sorted array (p in (0, 100]). */\nfunction nearestRank(sorted: ReadonlyArray<number>, p: number): number {\n if (sorted.length === 0) throw new Error('nearestRank requires at least one sample')\n const rank = Math.max(1, Math.ceil((p / 100) * sorted.length))\n return sorted[rank - 1] as number\n}\n\nexport function summarizeRecords(\n records: ReadonlyArray<Record<string, unknown>>,\n keyOf: (record: Record<string, unknown>) => string | null,\n metricFields: ReadonlyArray<string>,\n): PerfBaseline {\n const samples = new Map<string, Map<string, number[]>>()\n for (const record of records) {\n const key = keyOf(record)\n if (key === null) continue\n let byField = samples.get(key)\n if (!byField) {\n byField = new Map()\n samples.set(key, byField)\n }\n for (const field of metricFields) {\n const value = record[field]\n if (typeof value !== 'number' || Number.isNaN(value)) continue\n let values = byField.get(field)\n if (!values) {\n values = []\n byField.set(field, values)\n }\n values.push(value)\n }\n }\n const scenarios: Record<string, Record<string, PerfStat>> = {}\n for (const [key, byField] of samples) {\n const stats: Record<string, PerfStat> = {}\n for (const [field, values] of byField) {\n if (values.length === 0) continue\n const sorted = [...values].sort((a, b) => a - b)\n stats[field] = {\n p50: nearestRank(sorted, 50),\n p90: nearestRank(sorted, 90),\n n: sorted.length,\n }\n }\n scenarios[key] = stats\n }\n return { version: 1, scenarios }\n}\n\n/** Percent over baseline; baseline 0 → 0% when equal, Infinity when current grew. */\nfunction pctOver(currentValue: number, baselineValue: number): number {\n if (baselineValue === 0) {\n return currentValue === 0 ? 0 : Number.POSITIVE_INFINITY\n }\n return ((currentValue - baselineValue) / baselineValue) * 100\n}\n\nexport function gatePerf(\n current: PerfBaseline,\n baseline: PerfBaseline,\n options?: { tolerancePct?: number; minSamples?: number },\n): PerfGateResult {\n const tolerancePct = options?.tolerancePct ?? 10\n const minSamples = options?.minSamples ?? 3\n const regressions: PerfRegression[] = []\n const improvements: PerfRegression[] = []\n const missing = new Set<string>()\n\n for (const [scenarioKey, baselineStats] of Object.entries(baseline.scenarios)) {\n const currentStats = current.scenarios[scenarioKey]\n if (!currentStats) {\n missing.add(scenarioKey)\n continue\n }\n for (const [field, baselineStat] of Object.entries(baselineStats)) {\n const currentStat = currentStats[field]\n if (!currentStat || currentStat.n < minSamples) {\n // Under-sampled current data cannot gate — surface it instead of\n // pretending the scenario passed.\n missing.add(scenarioKey)\n continue\n }\n const overBy = {\n p50Pct: pctOver(currentStat.p50, baselineStat.p50),\n p90Pct: pctOver(currentStat.p90, baselineStat.p90),\n }\n const entry: PerfRegression = {\n scenarioKey,\n field,\n baseline: baselineStat,\n current: currentStat,\n overBy,\n }\n if (overBy.p50Pct > tolerancePct || overBy.p90Pct > tolerancePct) {\n regressions.push(entry)\n } else if (overBy.p50Pct < 0 && overBy.p90Pct < 0) {\n improvements.push(entry)\n }\n }\n }\n\n const newScenarios = Object.keys(current.scenarios).filter((key) => !(key in baseline.scenarios))\n\n return {\n succeeded: regressions.length === 0,\n regressions,\n improvements,\n missingScenarios: [...missing],\n newScenarios,\n }\n}\n"],"mappings":"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","names":["scenarioKey"]}
|
package/dist/contract/index.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fromClaudeCodeSession,
|
|
3
|
+
fromCodexSession,
|
|
4
|
+
fromKimiCodeSession,
|
|
5
|
+
fromOpenCodeSession,
|
|
6
|
+
fromPiSession,
|
|
7
|
+
fromPigraphSession,
|
|
8
|
+
parseCodeAgentJsonl
|
|
9
|
+
} from "../chunk-S42AWHMP.js";
|
|
1
10
|
import {
|
|
2
11
|
createHostedClient
|
|
3
12
|
} from "../chunk-ZZUXHH3R.js";
|
|
@@ -10,15 +19,6 @@ import {
|
|
|
10
19
|
paretoSignificanceGate,
|
|
11
20
|
runEval
|
|
12
21
|
} from "../chunk-6SOJM3VR.js";
|
|
13
|
-
import {
|
|
14
|
-
fromClaudeCodeSession,
|
|
15
|
-
fromCodexSession,
|
|
16
|
-
fromKimiCodeSession,
|
|
17
|
-
fromOpenCodeSession,
|
|
18
|
-
fromPiSession,
|
|
19
|
-
fromPigraphSession,
|
|
20
|
-
parseCodeAgentJsonl
|
|
21
|
-
} from "../chunk-S42AWHMP.js";
|
|
22
22
|
import {
|
|
23
23
|
analyzeRuns
|
|
24
24
|
} from "../chunk-Y47J2LJ3.js";
|
package/dist/fuzz.d.ts
CHANGED
|
@@ -167,6 +167,17 @@ interface CapsuleData<S> {
|
|
|
167
167
|
/** Runs whose cost was unknown (`costOf` returned null) — counted apart,
|
|
168
168
|
* never folded into `costUsd` as a fabricated $0. */
|
|
169
169
|
costUnknownRuns?: number;
|
|
170
|
+
/** Evaluations that threw (transport/backend failures). They consumed no
|
|
171
|
+
* run budget and scored nothing — an infra axis, never folded into
|
|
172
|
+
* robustness or reported as findings. */
|
|
173
|
+
evalErrors: number;
|
|
174
|
+
/** Present when the run stopped before its budget because consecutive
|
|
175
|
+
* eval errors tripped the circuit breaker (a dead backend must not burn
|
|
176
|
+
* the remaining budget). The capsule-so-far is complete and honest. */
|
|
177
|
+
stoppedEarly?: {
|
|
178
|
+
reason: 'eval-errors';
|
|
179
|
+
detail: string;
|
|
180
|
+
};
|
|
170
181
|
};
|
|
171
182
|
}
|
|
172
183
|
/**
|
|
@@ -190,6 +201,11 @@ type ExploreEvent<S> = {
|
|
|
190
201
|
} | {
|
|
191
202
|
type: 'finding';
|
|
192
203
|
finding: Finding<S>;
|
|
204
|
+
} | {
|
|
205
|
+
type: 'eval-error';
|
|
206
|
+
cell: Cell;
|
|
207
|
+
scenarioId: string;
|
|
208
|
+
message: string;
|
|
193
209
|
} | {
|
|
194
210
|
type: 'round';
|
|
195
211
|
runsUsed: number;
|
|
@@ -226,6 +242,9 @@ interface ExploreOptions<S> {
|
|
|
226
242
|
minimize?: (scenario: S, evaluate: Evaluator<S>, cell: Cell) => Promise<S> | S;
|
|
227
243
|
/** Max concurrent `evaluate` calls. Default 1. */
|
|
228
244
|
concurrency?: number;
|
|
245
|
+
/** Stop the run after this many CONSECUTIVE eval errors (a dead backend must
|
|
246
|
+
* not burn the remaining budget). Successes reset the streak. Default 5. */
|
|
247
|
+
maxConsecutiveEvalErrors?: number;
|
|
229
248
|
/** Cooperative cancellation. */
|
|
230
249
|
signal?: AbortSignal;
|
|
231
250
|
/** Progress stream. */
|
|
@@ -315,6 +334,13 @@ interface BuildCapsuleInput<S> {
|
|
|
315
334
|
costUsd: number;
|
|
316
335
|
costUnknownRuns: number;
|
|
317
336
|
};
|
|
337
|
+
/** Evaluations that threw — infra outcomes, never folded into robustness. */
|
|
338
|
+
evalErrors: number;
|
|
339
|
+
/** Set when the consecutive-error circuit breaker stopped the run early. */
|
|
340
|
+
stoppedEarly?: {
|
|
341
|
+
reason: 'eval-errors';
|
|
342
|
+
detail: string;
|
|
343
|
+
};
|
|
318
344
|
}
|
|
319
345
|
declare function buildCapsule<S>(input: BuildCapsuleInput<S>): CapsuleData<S>;
|
|
320
346
|
interface RenderCapsuleOptions {
|
|
@@ -356,6 +382,9 @@ declare class BehaviorExplorer<S> {
|
|
|
356
382
|
private readonly _findings;
|
|
357
383
|
private runsUsed;
|
|
358
384
|
private candidateFindings;
|
|
385
|
+
private evalErrors;
|
|
386
|
+
private consecutiveEvalErrors;
|
|
387
|
+
private stoppedEarly;
|
|
359
388
|
private rngState;
|
|
360
389
|
/** Accumulated KNOWN dollars — unknown-cost runs never inflate it. */
|
|
361
390
|
private spentKnownUsd;
|
package/dist/fuzz.js
CHANGED
|
@@ -69,7 +69,9 @@ function buildCapsule(input) {
|
|
|
69
69
|
candidateFindings: input.candidateFindings,
|
|
70
70
|
verifiedFindings: input.findings.length,
|
|
71
71
|
meanRobustness,
|
|
72
|
-
...input.cost ? { costUsd: input.cost.costUsd, costUnknownRuns: input.cost.costUnknownRuns } : {}
|
|
72
|
+
...input.cost ? { costUsd: input.cost.costUsd, costUnknownRuns: input.cost.costUnknownRuns } : {},
|
|
73
|
+
evalErrors: input.evalErrors,
|
|
74
|
+
...input.stoppedEarly ? { stoppedEarly: input.stoppedEarly } : {}
|
|
73
75
|
}
|
|
74
76
|
};
|
|
75
77
|
}
|
|
@@ -201,8 +203,10 @@ ${kpi("verified findings", String(s.verifiedFindings), s.verifiedFindings > 0 ?
|
|
|
201
203
|
${kpi("cells covered", `${s.cellsCovered}/${s.cellsTotal}`)}
|
|
202
204
|
${kpi("scenarios run", String(s.totalRuns))}
|
|
203
205
|
${cost}
|
|
206
|
+
${s.evalErrors > 0 ? kpi("eval errors", String(s.evalErrors), "#e5b566") : ""}
|
|
204
207
|
${lift}
|
|
205
208
|
</div>
|
|
209
|
+
${s.stoppedEarly ? `<div class="sub" style="color:#e5b566">stopped early: ${esc(s.stoppedEarly.detail)} \u2014 coverage below reflects the completed portion only</div>` : ""}
|
|
206
210
|
<h2>Coverage map</h2>
|
|
207
211
|
${heatmapHtml(capsule.coverage)}
|
|
208
212
|
<h2>Verified findings${s.candidateFindings > s.verifiedFindings ? ` \xB7 ${s.verifiedFindings} of ${s.candidateFindings} candidates passed the validity gates` : ""}</h2>
|
|
@@ -317,6 +321,9 @@ var BehaviorExplorer = class {
|
|
|
317
321
|
_findings = [];
|
|
318
322
|
runsUsed = 0;
|
|
319
323
|
candidateFindings = 0;
|
|
324
|
+
evalErrors = 0;
|
|
325
|
+
consecutiveEvalErrors = 0;
|
|
326
|
+
stoppedEarly;
|
|
320
327
|
rngState;
|
|
321
328
|
/** Accumulated KNOWN dollars — unknown-cost runs never inflate it. */
|
|
322
329
|
spentKnownUsd = 0;
|
|
@@ -400,7 +407,7 @@ var BehaviorExplorer = class {
|
|
|
400
407
|
const newFindings = [];
|
|
401
408
|
let runsThisStep = 0;
|
|
402
409
|
for (const alloc of allocations) {
|
|
403
|
-
if (this.runsUsed >= this.opts.budget || this.costExhausted() || this.opts.signal?.aborted)
|
|
410
|
+
if (this.runsUsed >= this.opts.budget || this.costExhausted() || this.stoppedEarly !== void 0 || this.opts.signal?.aborted)
|
|
404
411
|
break;
|
|
405
412
|
const cell = this.cellById.get(alloc.cellId);
|
|
406
413
|
if (!cell) continue;
|
|
@@ -422,39 +429,60 @@ var BehaviorExplorer = class {
|
|
|
422
429
|
await pMap(
|
|
423
430
|
toEval,
|
|
424
431
|
async (scenario) => {
|
|
425
|
-
if (this.runsUsed >= this.opts.budget || this.costExhausted() || this.opts.signal?.aborted)
|
|
432
|
+
if (this.runsUsed >= this.opts.budget || this.costExhausted() || this.stoppedEarly !== void 0 || this.opts.signal?.aborted)
|
|
426
433
|
return;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
this.archiveByBin.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
434
|
+
try {
|
|
435
|
+
const ev = await this.opts.evaluate(scenario, cell);
|
|
436
|
+
this.runsUsed++;
|
|
437
|
+
runsThisStep++;
|
|
438
|
+
this.consecutiveEvalErrors = 0;
|
|
439
|
+
this.recordRunCost(scenario, cell, ev);
|
|
440
|
+
const interest = this.objective.interest(ev, this.objectiveContext());
|
|
441
|
+
this.log.push({ cell, ev, interest, scenarioId: this.opts.scenarioId(scenario) });
|
|
442
|
+
this.opts.onProgress?.({ type: "evaluated", cell, scenario, evaluation: ev });
|
|
443
|
+
const bin = this.binId(cell, ev.descriptor);
|
|
444
|
+
const cur = this.archiveByBin.get(bin);
|
|
445
|
+
if (!cur || interest > cur.interest)
|
|
446
|
+
this.archiveByBin.set(bin, { binId: bin, cell, scenario, evaluation: ev, interest });
|
|
447
|
+
if (interest < this.threshold) return;
|
|
448
|
+
this.candidateFindings++;
|
|
449
|
+
if (this.opts.gates?.isValid && !await this.opts.gates.isValid(scenario, ev, cell))
|
|
450
|
+
return;
|
|
451
|
+
if (this.opts.gates?.isUncontaminated && !await this.opts.gates.isUncontaminated(scenario, ev, cell))
|
|
452
|
+
return;
|
|
453
|
+
const minimized = this.opts.minimize ? await this.opts.minimize(scenario, this.opts.evaluate, cell) : scenario;
|
|
454
|
+
const finding = {
|
|
455
|
+
id: this.opts.scenarioId(scenario),
|
|
456
|
+
cell,
|
|
457
|
+
scenario,
|
|
458
|
+
minimized,
|
|
459
|
+
text: this.opts.scenarioText?.(minimized),
|
|
460
|
+
evaluation: ev,
|
|
461
|
+
interest,
|
|
462
|
+
objective: this.objective.kind
|
|
463
|
+
};
|
|
464
|
+
this._findings.push(finding);
|
|
465
|
+
newFindings.push(finding);
|
|
466
|
+
this.opts.onProgress?.({ type: "finding", finding });
|
|
467
|
+
} catch (err) {
|
|
468
|
+
if (err instanceof RangeError) throw err;
|
|
469
|
+
this.evalErrors++;
|
|
470
|
+
this.consecutiveEvalErrors++;
|
|
471
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
472
|
+
this.opts.onProgress?.({
|
|
473
|
+
type: "eval-error",
|
|
474
|
+
cell,
|
|
475
|
+
scenarioId: this.opts.scenarioId(scenario),
|
|
476
|
+
message
|
|
477
|
+
});
|
|
478
|
+
const limit = this.opts.maxConsecutiveEvalErrors ?? 5;
|
|
479
|
+
if (this.consecutiveEvalErrors >= limit) {
|
|
480
|
+
this.stoppedEarly = {
|
|
481
|
+
reason: "eval-errors",
|
|
482
|
+
detail: `${this.consecutiveEvalErrors} consecutive eval errors (last: ${message})`
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
458
486
|
},
|
|
459
487
|
this.opts.concurrency ?? 1,
|
|
460
488
|
this.opts.signal
|
|
@@ -466,9 +494,9 @@ var BehaviorExplorer = class {
|
|
|
466
494
|
/** Loop `step()` until the run or dollar budget is spent, the signal aborts,
|
|
467
495
|
* or no progress is made. */
|
|
468
496
|
async run() {
|
|
469
|
-
while (this.runsUsed < this.opts.budget && !this.costExhausted() && !this.opts.signal?.aborted) {
|
|
497
|
+
while (this.runsUsed < this.opts.budget && !this.costExhausted() && this.stoppedEarly === void 0 && !this.opts.signal?.aborted) {
|
|
470
498
|
const { runs } = await this.step();
|
|
471
|
-
if (runs === 0) break;
|
|
499
|
+
if (runs === 0 && this.stoppedEarly === void 0) break;
|
|
472
500
|
}
|
|
473
501
|
return this.capsule();
|
|
474
502
|
}
|
|
@@ -489,7 +517,9 @@ var BehaviorExplorer = class {
|
|
|
489
517
|
findings: this._findings,
|
|
490
518
|
candidateFindings: this.candidateFindings,
|
|
491
519
|
runsUsed: this.runsUsed,
|
|
492
|
-
cost: this.opts.costOf ? { costUsd: this.spentKnownUsd, costUnknownRuns: this.costUnknownRuns } : void 0
|
|
520
|
+
cost: this.opts.costOf ? { costUsd: this.spentKnownUsd, costUnknownRuns: this.costUnknownRuns } : void 0,
|
|
521
|
+
evalErrors: this.evalErrors,
|
|
522
|
+
stoppedEarly: this.stoppedEarly
|
|
493
523
|
});
|
|
494
524
|
}
|
|
495
525
|
};
|