@tangle-network/agent-eval 0.84.0 → 0.86.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/CHANGELOG.md +14 -0
- package/dist/belief-state/index.d.ts +1 -0
- package/dist/belief-state/index.js +135 -2
- package/dist/belief-state/index.js.map +1 -1
- package/dist/{chunk-XQL22JDG.js → chunk-P2J6SOXT.js} +97 -5
- package/dist/chunk-P2J6SOXT.js.map +1 -0
- package/dist/index.d.ts +543 -4
- package/dist/index.js +768 -51
- package/dist/index.js.map +1 -1
- package/dist/openapi.json +1 -1
- package/dist/traces.d.ts +55 -1
- package/dist/traces.js +7 -1
- package/docs/building-doctrine.md +42 -0
- package/package.json +1 -1
- package/dist/chunk-XQL22JDG.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -230,6 +230,9 @@ import {
|
|
|
230
230
|
describeTraceInsightScope,
|
|
231
231
|
domainEvidencePattern,
|
|
232
232
|
exportRunAsOtlp,
|
|
233
|
+
extractUsage,
|
|
234
|
+
extractUsageFromResponse,
|
|
235
|
+
extractUsageFromSse,
|
|
233
236
|
flattenOtlpExportToNdjson,
|
|
234
237
|
inferDomainKeywords,
|
|
235
238
|
iterateRawCalls,
|
|
@@ -240,7 +243,7 @@ import {
|
|
|
240
243
|
scoreTraceInsightReadiness,
|
|
241
244
|
tokenizeDomainWords,
|
|
242
245
|
traceAnalystOnRunComplete
|
|
243
|
-
} from "./chunk-
|
|
246
|
+
} from "./chunk-P2J6SOXT.js";
|
|
244
247
|
import {
|
|
245
248
|
DEFAULT_REDACTION_RULES,
|
|
246
249
|
REDACTION_VERSION,
|
|
@@ -1751,6 +1754,107 @@ function canonicalize2(value) {
|
|
|
1751
1754
|
return out;
|
|
1752
1755
|
}
|
|
1753
1756
|
|
|
1757
|
+
// src/integrity/preflight.ts
|
|
1758
|
+
function stripSlash(url) {
|
|
1759
|
+
return url.replace(/\/+$/, "");
|
|
1760
|
+
}
|
|
1761
|
+
function errorMessage(body) {
|
|
1762
|
+
if (body == null || typeof body !== "object") return null;
|
|
1763
|
+
const b = body;
|
|
1764
|
+
if (b.error && typeof b.error.message === "string") return b.error.message;
|
|
1765
|
+
if (typeof b.message === "string") return b.message;
|
|
1766
|
+
return null;
|
|
1767
|
+
}
|
|
1768
|
+
async function preflightModels(opts) {
|
|
1769
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
1770
|
+
const baseUrl = stripSlash(opts.baseUrl);
|
|
1771
|
+
const authHeaders = { authorization: `Bearer ${opts.apiKey}` };
|
|
1772
|
+
let served;
|
|
1773
|
+
try {
|
|
1774
|
+
const res = await fetchImpl(`${baseUrl}/models`, { method: "GET", headers: authHeaders });
|
|
1775
|
+
if (!res.ok) {
|
|
1776
|
+
const text = await res.text().catch(() => "");
|
|
1777
|
+
return {
|
|
1778
|
+
succeeded: false,
|
|
1779
|
+
value: null,
|
|
1780
|
+
error: `preflightModels: GET ${baseUrl}/models \u2192 ${res.status} ${text.slice(0, 400)}`
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
const body = await res.json();
|
|
1784
|
+
const ids = Array.isArray(body.data) ? body.data : [];
|
|
1785
|
+
served = new Set(ids.map((m) => m.id).filter((id) => typeof id === "string"));
|
|
1786
|
+
} catch (err) {
|
|
1787
|
+
return {
|
|
1788
|
+
succeeded: false,
|
|
1789
|
+
value: null,
|
|
1790
|
+
error: `preflightModels: GET ${baseUrl}/models failed \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
const results = [];
|
|
1794
|
+
for (const model of opts.models) {
|
|
1795
|
+
const listed = served.has(model);
|
|
1796
|
+
if (!opts.probe) {
|
|
1797
|
+
results.push({ model, listed, served: null, status: null, detail: null });
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
try {
|
|
1801
|
+
const res = await fetchImpl(`${baseUrl}/chat/completions`, {
|
|
1802
|
+
method: "POST",
|
|
1803
|
+
headers: { ...authHeaders, "content-type": "application/json" },
|
|
1804
|
+
body: JSON.stringify({
|
|
1805
|
+
model,
|
|
1806
|
+
messages: [{ role: "user", content: "ping" }],
|
|
1807
|
+
max_tokens: 5
|
|
1808
|
+
})
|
|
1809
|
+
});
|
|
1810
|
+
let detail = null;
|
|
1811
|
+
if (!res.ok) {
|
|
1812
|
+
const body = await res.json().catch(() => null);
|
|
1813
|
+
detail = errorMessage(body);
|
|
1814
|
+
}
|
|
1815
|
+
results.push({ model, listed, served: res.ok, status: res.status, detail });
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
return {
|
|
1818
|
+
succeeded: false,
|
|
1819
|
+
value: null,
|
|
1820
|
+
error: `preflightModels: probe POST ${baseUrl}/chat/completions (model ${model}) failed \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return { succeeded: true, value: results, error: null };
|
|
1825
|
+
}
|
|
1826
|
+
var ModelsUnreachableError = class extends AgentEvalError {
|
|
1827
|
+
constructor(message, results) {
|
|
1828
|
+
super("config", message);
|
|
1829
|
+
this.results = results;
|
|
1830
|
+
this.name = "ModelsUnreachableError";
|
|
1831
|
+
}
|
|
1832
|
+
results;
|
|
1833
|
+
};
|
|
1834
|
+
function describeFailure(r) {
|
|
1835
|
+
if (!r.listed) {
|
|
1836
|
+
const probeNote = r.served === false ? ` (probe ${r.status}${r.detail ? `: ${r.detail}` : ""})` : "";
|
|
1837
|
+
return `${r.model}: not in /models${probeNote}`;
|
|
1838
|
+
}
|
|
1839
|
+
return `${r.model}: listed but probe ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}`;
|
|
1840
|
+
}
|
|
1841
|
+
async function assertModelsServed(opts) {
|
|
1842
|
+
const outcome = await preflightModels(opts);
|
|
1843
|
+
if (!outcome.succeeded || outcome.value === null) {
|
|
1844
|
+
throw new ConfigError(
|
|
1845
|
+
outcome.error ?? "assertModelsServed: preflight failed without an error message"
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
const dead = outcome.value.filter((r) => !r.listed || r.served === false);
|
|
1849
|
+
if (dead.length > 0) {
|
|
1850
|
+
throw new ModelsUnreachableError(
|
|
1851
|
+
`assertModelsServed: ${dead.length}/${outcome.value.length} model(s) unreachable on the router \u2014 ${dead.map(describeFailure).join("; ")}`,
|
|
1852
|
+
outcome.value
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
return outcome.value;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1754
1858
|
// src/integrity/single-backend.ts
|
|
1755
1859
|
var SingleBackendError = class extends AgentEvalError {
|
|
1756
1860
|
constructor(message, report) {
|
|
@@ -1760,7 +1864,7 @@ var SingleBackendError = class extends AgentEvalError {
|
|
|
1760
1864
|
}
|
|
1761
1865
|
report;
|
|
1762
1866
|
};
|
|
1763
|
-
function
|
|
1867
|
+
function stripSlash2(url) {
|
|
1764
1868
|
return url.replace(/\/+$/, "");
|
|
1765
1869
|
}
|
|
1766
1870
|
function assertSingleBackend(agent, judge, opts = {}) {
|
|
@@ -1768,7 +1872,7 @@ function assertSingleBackend(agent, judge, opts = {}) {
|
|
|
1768
1872
|
if (agent.kind !== judge.kind) {
|
|
1769
1873
|
divergences.push({ field: "kind", agent: agent.kind, judge: judge.kind });
|
|
1770
1874
|
}
|
|
1771
|
-
if (
|
|
1875
|
+
if (stripSlash2(agent.baseUrl) !== stripSlash2(judge.baseUrl)) {
|
|
1772
1876
|
divergences.push({ field: "baseUrl", agent: agent.baseUrl, judge: judge.baseUrl });
|
|
1773
1877
|
}
|
|
1774
1878
|
if (agent.model !== judge.model) {
|
|
@@ -2290,16 +2394,16 @@ function aggregatePrReviewScore(dimensions, weights = {}) {
|
|
|
2290
2394
|
return (merged.recall * clamp01(dimensions.recall) + merged.precision * clamp01(dimensions.precision) + merged.actionability * clamp01(dimensions.actionability) + merged.severityCalibration * clamp01(dimensions.severityCalibration) + merged.lowNoise * clamp01(dimensions.lowNoise)) / weightSum;
|
|
2291
2395
|
}
|
|
2292
2396
|
function matchReferenceFindings(references, comments) {
|
|
2293
|
-
const
|
|
2397
|
+
const matches2 = [];
|
|
2294
2398
|
const usedCommentIds = /* @__PURE__ */ new Set();
|
|
2295
2399
|
for (const reference of references) {
|
|
2296
2400
|
const candidates = comments.filter((comment) => !usedCommentIds.has(comment.id)).map((comment) => ({ comment, score: matchScore(reference, comment) })).filter(({ score }) => score >= 0.55).sort((a, b) => b.score - a.score);
|
|
2297
2401
|
const best = candidates[0];
|
|
2298
2402
|
if (!best) continue;
|
|
2299
2403
|
usedCommentIds.add(best.comment.id);
|
|
2300
|
-
|
|
2404
|
+
matches2.push({ referenceId: reference.id, commentId: best.comment.id, score: best.score });
|
|
2301
2405
|
}
|
|
2302
|
-
return
|
|
2406
|
+
return matches2;
|
|
2303
2407
|
}
|
|
2304
2408
|
function matchScore(reference, comment) {
|
|
2305
2409
|
let score = 0;
|
|
@@ -2326,9 +2430,9 @@ function isActionableComment(comment) {
|
|
|
2326
2430
|
body
|
|
2327
2431
|
);
|
|
2328
2432
|
}
|
|
2329
|
-
function isSeverityAligned(comment, references,
|
|
2433
|
+
function isSeverityAligned(comment, references, matches2) {
|
|
2330
2434
|
if (!comment.severity) return false;
|
|
2331
|
-
const match =
|
|
2435
|
+
const match = matches2.find((candidate) => candidate.commentId === comment.id);
|
|
2332
2436
|
if (!match) return comment.severity === "nit" || comment.severity === "low";
|
|
2333
2437
|
const reference = references.find((candidate) => candidate.id === match.referenceId);
|
|
2334
2438
|
if (!reference) return false;
|
|
@@ -2624,28 +2728,28 @@ function analyzeAntiSlop(outputs, config) {
|
|
|
2624
2728
|
}
|
|
2625
2729
|
}
|
|
2626
2730
|
for (const re of config.hedgingPatterns) {
|
|
2627
|
-
const
|
|
2731
|
+
const matches2 = output.match(
|
|
2628
2732
|
new RegExp(re, re.flags.includes("g") ? re.flags : `${re.flags}g`)
|
|
2629
2733
|
);
|
|
2630
|
-
if (
|
|
2631
|
-
counts.hedging +=
|
|
2734
|
+
if (matches2) {
|
|
2735
|
+
counts.hedging += matches2.length;
|
|
2632
2736
|
issues.push({
|
|
2633
2737
|
category: "hedging",
|
|
2634
|
-
detail: `${
|
|
2635
|
-
example:
|
|
2738
|
+
detail: `${matches2.length}x ${re.source}`,
|
|
2739
|
+
example: matches2[0]
|
|
2636
2740
|
});
|
|
2637
2741
|
}
|
|
2638
2742
|
}
|
|
2639
2743
|
for (const re of config.apologyPatterns) {
|
|
2640
|
-
const
|
|
2744
|
+
const matches2 = output.match(
|
|
2641
2745
|
new RegExp(re, re.flags.includes("g") ? re.flags : `${re.flags}g`)
|
|
2642
2746
|
);
|
|
2643
|
-
if (
|
|
2644
|
-
counts.apology +=
|
|
2747
|
+
if (matches2) {
|
|
2748
|
+
counts.apology += matches2.length;
|
|
2645
2749
|
issues.push({
|
|
2646
2750
|
category: "apology",
|
|
2647
|
-
detail: `${
|
|
2648
|
-
example:
|
|
2751
|
+
detail: `${matches2.length}x ${re.source}`,
|
|
2752
|
+
example: matches2[0]
|
|
2649
2753
|
});
|
|
2650
2754
|
}
|
|
2651
2755
|
}
|
|
@@ -2915,14 +3019,14 @@ async function runHarnessExperiment(config) {
|
|
|
2915
3019
|
const score = config.score ?? ((trace) => critic.scoreTrace(trace));
|
|
2916
3020
|
const results = await mapLimit(jobs, config.parallelism ?? 1, async (request) => {
|
|
2917
3021
|
const trace = await config.adapter.run(request);
|
|
2918
|
-
const
|
|
3022
|
+
const runScore4 = await score(trace, request);
|
|
2919
3023
|
const result = {
|
|
2920
3024
|
variant: request.variant,
|
|
2921
3025
|
scenario: request.scenario,
|
|
2922
3026
|
trialIndex: request.trialIndex,
|
|
2923
3027
|
trace,
|
|
2924
|
-
score:
|
|
2925
|
-
aggregate: aggregateRunScore(
|
|
3028
|
+
score: runScore4,
|
|
3029
|
+
aggregate: aggregateRunScore(runScore4, config.weights)
|
|
2926
3030
|
};
|
|
2927
3031
|
await config.onResult?.(result);
|
|
2928
3032
|
return result;
|
|
@@ -3568,13 +3672,118 @@ var BudgetGuard = class {
|
|
|
3568
3672
|
}
|
|
3569
3673
|
};
|
|
3570
3674
|
|
|
3675
|
+
// src/cost-ledger.ts
|
|
3676
|
+
function modelPriceKey(model) {
|
|
3677
|
+
return isModelPriced(model) ? model : null;
|
|
3678
|
+
}
|
|
3679
|
+
function costForUsage(model, usage) {
|
|
3680
|
+
assertNonNegative(usage.inputTokens, "inputTokens");
|
|
3681
|
+
assertNonNegative(usage.outputTokens, "outputTokens");
|
|
3682
|
+
if (usage.cachedTokens !== void 0) assertNonNegative(usage.cachedTokens, "cachedTokens");
|
|
3683
|
+
const pricing = resolveModelPricing(model);
|
|
3684
|
+
if (!pricing) return { costUsd: 0, costUnknown: true };
|
|
3685
|
+
const billedInput = usage.inputTokens + (usage.cachedTokens ?? 0);
|
|
3686
|
+
return { costUsd: estimateCost(billedInput, usage.outputTokens, model), costUnknown: false };
|
|
3687
|
+
}
|
|
3688
|
+
var CostLedger = class {
|
|
3689
|
+
entries = [];
|
|
3690
|
+
completedTasks = 0;
|
|
3691
|
+
/**
|
|
3692
|
+
* Record one LLM call. The cost is computed from pricing unless
|
|
3693
|
+
* `actualCostUsd` is supplied (a finite observed cost from the provider
|
|
3694
|
+
* response), in which case `costUnknown` is false regardless of pricing.
|
|
3695
|
+
*/
|
|
3696
|
+
record(input) {
|
|
3697
|
+
const { costUsd, costUnknown } = costForUsage(input.model, input.usage);
|
|
3698
|
+
const hasActual = typeof input.actualCostUsd === "number" && Number.isFinite(input.actualCostUsd);
|
|
3699
|
+
if (hasActual) assertNonNegative(input.actualCostUsd, "actualCostUsd");
|
|
3700
|
+
const entry = {
|
|
3701
|
+
model: input.model,
|
|
3702
|
+
channel: input.channel,
|
|
3703
|
+
inputTokens: input.usage.inputTokens,
|
|
3704
|
+
outputTokens: input.usage.outputTokens,
|
|
3705
|
+
cachedTokens: input.usage.cachedTokens,
|
|
3706
|
+
costUsd: hasActual ? input.actualCostUsd : costUsd,
|
|
3707
|
+
costUnknown: hasActual ? false : costUnknown,
|
|
3708
|
+
actualCostUsd: hasActual ? input.actualCostUsd : void 0,
|
|
3709
|
+
tags: input.tags,
|
|
3710
|
+
timestamp: input.timestamp ?? Date.now()
|
|
3711
|
+
};
|
|
3712
|
+
this.entries.push(entry);
|
|
3713
|
+
return entry;
|
|
3714
|
+
}
|
|
3715
|
+
/** Increment the completed-task counter (used for cost-per-completed-task). */
|
|
3716
|
+
markCompleted(count = 1) {
|
|
3717
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
3718
|
+
throw new ValidationError(
|
|
3719
|
+
`CostLedger.markCompleted: count must be a non-negative integer, got ${count}`
|
|
3720
|
+
);
|
|
3721
|
+
}
|
|
3722
|
+
this.completedTasks += count;
|
|
3723
|
+
}
|
|
3724
|
+
list() {
|
|
3725
|
+
return [...this.entries];
|
|
3726
|
+
}
|
|
3727
|
+
summary() {
|
|
3728
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
3729
|
+
const unpriced = /* @__PURE__ */ new Set();
|
|
3730
|
+
let totalCost = 0;
|
|
3731
|
+
let inputTokens = 0;
|
|
3732
|
+
let outputTokens = 0;
|
|
3733
|
+
let cachedTokens = 0;
|
|
3734
|
+
for (const e of this.entries) {
|
|
3735
|
+
totalCost += e.costUsd;
|
|
3736
|
+
inputTokens += e.inputTokens;
|
|
3737
|
+
outputTokens += e.outputTokens;
|
|
3738
|
+
cachedTokens += e.cachedTokens ?? 0;
|
|
3739
|
+
if (e.costUnknown) unpriced.add(e.model);
|
|
3740
|
+
const roll = byChannel.get(e.channel) ?? {
|
|
3741
|
+
channel: e.channel,
|
|
3742
|
+
calls: 0,
|
|
3743
|
+
inputTokens: 0,
|
|
3744
|
+
outputTokens: 0,
|
|
3745
|
+
cachedTokens: 0,
|
|
3746
|
+
costUsd: 0,
|
|
3747
|
+
unpricedCalls: 0
|
|
3748
|
+
};
|
|
3749
|
+
roll.calls += 1;
|
|
3750
|
+
roll.inputTokens += e.inputTokens;
|
|
3751
|
+
roll.outputTokens += e.outputTokens;
|
|
3752
|
+
roll.cachedTokens += e.cachedTokens ?? 0;
|
|
3753
|
+
roll.costUsd += e.costUsd;
|
|
3754
|
+
if (e.costUnknown) roll.unpricedCalls += 1;
|
|
3755
|
+
byChannel.set(e.channel, roll);
|
|
3756
|
+
}
|
|
3757
|
+
return {
|
|
3758
|
+
totalCalls: this.entries.length,
|
|
3759
|
+
inputTokens,
|
|
3760
|
+
outputTokens,
|
|
3761
|
+
cachedTokens,
|
|
3762
|
+
totalCostUsd: totalCost,
|
|
3763
|
+
byChannel: [...byChannel.values()].sort((a, b) => a.channel.localeCompare(b.channel)),
|
|
3764
|
+
unpricedModels: [...unpriced].sort(),
|
|
3765
|
+
fullyPriced: unpriced.size === 0
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
/** Total spend divided by completed tasks; null when nothing completed. */
|
|
3769
|
+
costPerCompletedTask() {
|
|
3770
|
+
if (this.completedTasks === 0) return null;
|
|
3771
|
+
return this.summary().totalCostUsd / this.completedTasks;
|
|
3772
|
+
}
|
|
3773
|
+
};
|
|
3774
|
+
function assertNonNegative(n, name) {
|
|
3775
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
3776
|
+
throw new ValidationError(`CostLedger: ${name} must be a non-negative finite number, got ${n}`);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3571
3780
|
// src/cost-tracker.ts
|
|
3572
3781
|
var CostTracker = class {
|
|
3573
3782
|
byScenario = /* @__PURE__ */ new Map();
|
|
3574
3783
|
record(entry) {
|
|
3575
3784
|
const full = { timestamp: entry.timestamp ?? Date.now(), ...entry };
|
|
3576
|
-
|
|
3577
|
-
|
|
3785
|
+
assertNonNegative2(full.inputTokens, "inputTokens");
|
|
3786
|
+
assertNonNegative2(full.outputTokens, "outputTokens");
|
|
3578
3787
|
let bucket = this.byScenario.get(full.scenarioId);
|
|
3579
3788
|
if (!bucket) {
|
|
3580
3789
|
bucket = {
|
|
@@ -3653,12 +3862,433 @@ function costFor(entry) {
|
|
|
3653
3862
|
}
|
|
3654
3863
|
return estimateCost(entry.inputTokens, entry.outputTokens, entry.model);
|
|
3655
3864
|
}
|
|
3656
|
-
function
|
|
3865
|
+
function assertNonNegative2(n, name) {
|
|
3657
3866
|
if (!Number.isFinite(n) || n < 0) {
|
|
3658
3867
|
throw new Error(`CostTracker: ${name} must be a non-negative finite number, got ${n}`);
|
|
3659
3868
|
}
|
|
3660
3869
|
}
|
|
3661
3870
|
|
|
3871
|
+
// src/eval-trace-store.ts
|
|
3872
|
+
function runScore(record) {
|
|
3873
|
+
const { holdoutScore, searchScore } = record.outcome;
|
|
3874
|
+
if (typeof holdoutScore === "number") return holdoutScore;
|
|
3875
|
+
if (typeof searchScore === "number") return searchScore;
|
|
3876
|
+
throw new ValidationError(
|
|
3877
|
+
`EvalTraceStore: run ${record.runId} has neither holdoutScore nor searchScore`
|
|
3878
|
+
);
|
|
3879
|
+
}
|
|
3880
|
+
function matches(record, f) {
|
|
3881
|
+
if (f.experimentId && record.experimentId !== f.experimentId) return false;
|
|
3882
|
+
if (f.candidateId && record.candidateId !== f.candidateId) return false;
|
|
3883
|
+
if (f.scenarioId && record.scenarioId !== f.scenarioId) return false;
|
|
3884
|
+
if (f.model && record.model !== f.model) return false;
|
|
3885
|
+
if (f.splitTag && record.splitTag !== f.splitTag) return false;
|
|
3886
|
+
if (f.minScore !== void 0 && runScore(record) < f.minScore) return false;
|
|
3887
|
+
if (f.maxScore !== void 0 && runScore(record) > f.maxScore) return false;
|
|
3888
|
+
if (f.rawEquals && record.outcome.raw[f.rawEquals.key] !== f.rawEquals.value) return false;
|
|
3889
|
+
if (f.where && !f.where(record)) return false;
|
|
3890
|
+
return true;
|
|
3891
|
+
}
|
|
3892
|
+
function inMemoryRunRecordBackend(initial = []) {
|
|
3893
|
+
const rows = initial.map((r) => validateRunRecord(r));
|
|
3894
|
+
return {
|
|
3895
|
+
async append(record) {
|
|
3896
|
+
rows.push(record);
|
|
3897
|
+
},
|
|
3898
|
+
async load() {
|
|
3899
|
+
return [...rows];
|
|
3900
|
+
}
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
function jsonlRunRecordBackend(path, opts = {}) {
|
|
3904
|
+
return {
|
|
3905
|
+
async append(record) {
|
|
3906
|
+
const fs2 = await import("fs/promises");
|
|
3907
|
+
const pathMod = await import("path");
|
|
3908
|
+
await fs2.mkdir(pathMod.dirname(path), { recursive: true });
|
|
3909
|
+
await fs2.appendFile(path, `${JSON.stringify(record)}
|
|
3910
|
+
`, "utf8");
|
|
3911
|
+
},
|
|
3912
|
+
async load() {
|
|
3913
|
+
const fs2 = await import("fs/promises");
|
|
3914
|
+
let raw;
|
|
3915
|
+
try {
|
|
3916
|
+
raw = await fs2.readFile(path, "utf8");
|
|
3917
|
+
} catch (err) {
|
|
3918
|
+
if (err.code === "ENOENT") return [];
|
|
3919
|
+
throw err;
|
|
3920
|
+
}
|
|
3921
|
+
const out = [];
|
|
3922
|
+
let lineNo = 0;
|
|
3923
|
+
for (const line of raw.split("\n")) {
|
|
3924
|
+
lineNo++;
|
|
3925
|
+
if (!line.trim()) continue;
|
|
3926
|
+
let parsed;
|
|
3927
|
+
try {
|
|
3928
|
+
parsed = JSON.parse(line);
|
|
3929
|
+
} catch (err) {
|
|
3930
|
+
if (opts.skipInvalid) continue;
|
|
3931
|
+
throw new ValidationError(`EvalTraceStore: ${path}:${lineNo} is not valid JSON`);
|
|
3932
|
+
}
|
|
3933
|
+
if (opts.skipInvalid) {
|
|
3934
|
+
if (isRunRecord(parsed)) out.push(parsed);
|
|
3935
|
+
continue;
|
|
3936
|
+
}
|
|
3937
|
+
try {
|
|
3938
|
+
out.push(validateRunRecord(parsed));
|
|
3939
|
+
} catch (err) {
|
|
3940
|
+
throw new ValidationError(
|
|
3941
|
+
`EvalTraceStore: ${path}:${lineNo} is not a valid RunRecord \u2014 ${err.message}`
|
|
3942
|
+
);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
return out;
|
|
3946
|
+
}
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3949
|
+
var EvalTraceStore = class {
|
|
3950
|
+
backend;
|
|
3951
|
+
constructor(backend = inMemoryRunRecordBackend()) {
|
|
3952
|
+
this.backend = backend;
|
|
3953
|
+
}
|
|
3954
|
+
/** Validate and append one run. Throws on an invalid record — the corpus
|
|
3955
|
+
* stays paper-grade. */
|
|
3956
|
+
async append(record) {
|
|
3957
|
+
await this.backend.append(validateRunRecord(record));
|
|
3958
|
+
}
|
|
3959
|
+
async all() {
|
|
3960
|
+
return this.backend.load();
|
|
3961
|
+
}
|
|
3962
|
+
async query(filter = {}) {
|
|
3963
|
+
const rows = await this.backend.load();
|
|
3964
|
+
return rows.filter((r) => matches(r, filter));
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Highest-scoring run for a scenario (optionally restricted to a candidate).
|
|
3968
|
+
* Returns null when no run matches. Ties resolve to the earliest-appended run
|
|
3969
|
+
* so the result is stable.
|
|
3970
|
+
*/
|
|
3971
|
+
async getBest(scenarioId, opts = {}) {
|
|
3972
|
+
const rows = await this.query({
|
|
3973
|
+
scenarioId,
|
|
3974
|
+
candidateId: opts.candidateId,
|
|
3975
|
+
splitTag: opts.splitTag
|
|
3976
|
+
});
|
|
3977
|
+
if (rows.length === 0) return null;
|
|
3978
|
+
let best = rows[0];
|
|
3979
|
+
let bestScore = runScore(best);
|
|
3980
|
+
for (let i = 1; i < rows.length; i++) {
|
|
3981
|
+
const s = runScore(rows[i]);
|
|
3982
|
+
if (s > bestScore) {
|
|
3983
|
+
best = rows[i];
|
|
3984
|
+
bestScore = s;
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
return best;
|
|
3988
|
+
}
|
|
3989
|
+
/**
|
|
3990
|
+
* Compare two candidates on the scenarios they BOTH ran. When a candidate
|
|
3991
|
+
* ran a scenario more than once, its best `runScore` for that scenario is
|
|
3992
|
+
* used. Throws when there is no paired scenario — an unpaired "comparison" is
|
|
3993
|
+
* not one.
|
|
3994
|
+
*/
|
|
3995
|
+
async compareRuns(candidateA, candidateB) {
|
|
3996
|
+
if (candidateA === candidateB) {
|
|
3997
|
+
throw new ValidationError(
|
|
3998
|
+
`EvalTraceStore.compareRuns: candidates must differ ("${candidateA}")`
|
|
3999
|
+
);
|
|
4000
|
+
}
|
|
4001
|
+
const rows = await this.backend.load();
|
|
4002
|
+
const bestByScenario = (candidate) => {
|
|
4003
|
+
const m = /* @__PURE__ */ new Map();
|
|
4004
|
+
for (const r of rows) {
|
|
4005
|
+
if (r.candidateId !== candidate) continue;
|
|
4006
|
+
const sid = r.scenarioId;
|
|
4007
|
+
if (!sid) continue;
|
|
4008
|
+
const s = runScore(r);
|
|
4009
|
+
const prev = m.get(sid);
|
|
4010
|
+
if (prev === void 0 || s > prev) m.set(sid, s);
|
|
4011
|
+
}
|
|
4012
|
+
return m;
|
|
4013
|
+
};
|
|
4014
|
+
const aScores = bestByScenario(candidateA);
|
|
4015
|
+
const bScores = bestByScenario(candidateB);
|
|
4016
|
+
const paired = [...aScores.keys()].filter((sid) => bScores.has(sid)).sort();
|
|
4017
|
+
if (paired.length === 0) {
|
|
4018
|
+
throw new ValidationError(
|
|
4019
|
+
`EvalTraceStore.compareRuns: "${candidateA}" and "${candidateB}" share no scenario (need scenarioId on records)`
|
|
4020
|
+
);
|
|
4021
|
+
}
|
|
4022
|
+
let sumA = 0;
|
|
4023
|
+
let sumB = 0;
|
|
4024
|
+
let bWins = 0;
|
|
4025
|
+
let aWins = 0;
|
|
4026
|
+
let ties = 0;
|
|
4027
|
+
for (const sid of paired) {
|
|
4028
|
+
const sa = aScores.get(sid);
|
|
4029
|
+
const sb = bScores.get(sid);
|
|
4030
|
+
sumA += sa;
|
|
4031
|
+
sumB += sb;
|
|
4032
|
+
if (sb > sa) bWins++;
|
|
4033
|
+
else if (sb < sa) aWins++;
|
|
4034
|
+
else ties++;
|
|
4035
|
+
}
|
|
4036
|
+
const meanA = sumA / paired.length;
|
|
4037
|
+
const meanB = sumB / paired.length;
|
|
4038
|
+
return {
|
|
4039
|
+
a: candidateA,
|
|
4040
|
+
b: candidateB,
|
|
4041
|
+
pairedScenarioIds: paired,
|
|
4042
|
+
meanA,
|
|
4043
|
+
meanB,
|
|
4044
|
+
meanDelta: meanB - meanA,
|
|
4045
|
+
bWins,
|
|
4046
|
+
ties,
|
|
4047
|
+
aWins
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
4050
|
+
};
|
|
4051
|
+
|
|
4052
|
+
// src/experiment-tracker.ts
|
|
4053
|
+
import { execSync } from "child_process";
|
|
4054
|
+
var DEFAULTS = {
|
|
4055
|
+
keepThreshold: 5,
|
|
4056
|
+
regressionThreshold: 5,
|
|
4057
|
+
iqrUnstableAbove: 10,
|
|
4058
|
+
stddevUnstableAbove: Number.POSITIVE_INFINITY,
|
|
4059
|
+
minRepsForVerdict: 3
|
|
4060
|
+
};
|
|
4061
|
+
function resolveThresholds(t) {
|
|
4062
|
+
const r = { ...DEFAULTS, ...t ?? {} };
|
|
4063
|
+
if (r.keepThreshold < 0) {
|
|
4064
|
+
throw new ValidationError(
|
|
4065
|
+
`experiment-tracker: keepThreshold must be >= 0, got ${r.keepThreshold}`
|
|
4066
|
+
);
|
|
4067
|
+
}
|
|
4068
|
+
if (r.regressionThreshold < 0) {
|
|
4069
|
+
throw new ValidationError(
|
|
4070
|
+
`experiment-tracker: regressionThreshold must be >= 0, got ${r.regressionThreshold}`
|
|
4071
|
+
);
|
|
4072
|
+
}
|
|
4073
|
+
if (r.minRepsForVerdict < 1) {
|
|
4074
|
+
throw new ValidationError(
|
|
4075
|
+
`experiment-tracker: minRepsForVerdict must be >= 1, got ${r.minRepsForVerdict}`
|
|
4076
|
+
);
|
|
4077
|
+
}
|
|
4078
|
+
return r;
|
|
4079
|
+
}
|
|
4080
|
+
function median(sorted) {
|
|
4081
|
+
const n = sorted.length;
|
|
4082
|
+
if (n === 0) return 0;
|
|
4083
|
+
const mid = Math.floor(n / 2);
|
|
4084
|
+
return n % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
4085
|
+
}
|
|
4086
|
+
function stddev(values, mean5) {
|
|
4087
|
+
if (values.length < 2) return 0;
|
|
4088
|
+
const variance = values.reduce((acc, v) => acc + (v - mean5) ** 2, 0) / values.length;
|
|
4089
|
+
return Math.sqrt(variance);
|
|
4090
|
+
}
|
|
4091
|
+
function computeExperimentStats(reps, thresholds) {
|
|
4092
|
+
const t = resolveThresholds(thresholds);
|
|
4093
|
+
const n = reps.length;
|
|
4094
|
+
if (n === 0) {
|
|
4095
|
+
return {
|
|
4096
|
+
median: 0,
|
|
4097
|
+
mean: 0,
|
|
4098
|
+
min: 0,
|
|
4099
|
+
max: 0,
|
|
4100
|
+
iqr: 0,
|
|
4101
|
+
stddev: 0,
|
|
4102
|
+
passRate: null,
|
|
4103
|
+
n: 0,
|
|
4104
|
+
stable: false
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
const scores2 = reps.map((r) => {
|
|
4108
|
+
if (!Number.isFinite(r.score)) {
|
|
4109
|
+
throw new ValidationError(`experiment-tracker: rep ${r.rep} has non-finite score ${r.score}`);
|
|
4110
|
+
}
|
|
4111
|
+
return r.score;
|
|
4112
|
+
});
|
|
4113
|
+
const sorted = [...scores2].sort((a, b) => a - b);
|
|
4114
|
+
const mean5 = scores2.reduce((s, v) => s + v, 0) / n;
|
|
4115
|
+
const sd = stddev(scores2, mean5);
|
|
4116
|
+
const spread = iqr(scores2);
|
|
4117
|
+
const rated = reps.filter((r) => typeof r.passed === "boolean");
|
|
4118
|
+
const passRate = rated.length === 0 ? null : rated.filter((r) => r.passed).length / rated.length;
|
|
4119
|
+
const stable = spread < t.iqrUnstableAbove && sd < t.stddevUnstableAbove;
|
|
4120
|
+
return {
|
|
4121
|
+
median: median(sorted),
|
|
4122
|
+
mean: mean5,
|
|
4123
|
+
min: sorted[0],
|
|
4124
|
+
max: sorted[n - 1],
|
|
4125
|
+
iqr: spread,
|
|
4126
|
+
stddev: sd,
|
|
4127
|
+
passRate,
|
|
4128
|
+
n,
|
|
4129
|
+
stable
|
|
4130
|
+
};
|
|
4131
|
+
}
|
|
4132
|
+
function improvementVerdict(candidate, parent, thresholds) {
|
|
4133
|
+
const t = resolveThresholds(thresholds);
|
|
4134
|
+
if (!parent) {
|
|
4135
|
+
return {
|
|
4136
|
+
verdict: "ITERATE",
|
|
4137
|
+
medianDelta: null,
|
|
4138
|
+
reason: "no parent experiment to compare against"
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
if (candidate.n < t.minRepsForVerdict || parent.n < t.minRepsForVerdict) {
|
|
4142
|
+
return {
|
|
4143
|
+
verdict: "ITERATE",
|
|
4144
|
+
medianDelta: null,
|
|
4145
|
+
reason: `need >= ${t.minRepsForVerdict} reps on both sides (candidate n=${candidate.n}, parent n=${parent.n})`
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
if (!candidate.stable) {
|
|
4149
|
+
return {
|
|
4150
|
+
verdict: "NOISE",
|
|
4151
|
+
medianDelta: candidate.median - parent.median,
|
|
4152
|
+
reason: `candidate unstable (iqr=${candidate.iqr}, stddev=${candidate.stddev.toFixed(2)})`
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
const medianDelta2 = candidate.median - parent.median;
|
|
4156
|
+
if (medianDelta2 > t.keepThreshold) {
|
|
4157
|
+
return { verdict: "KEEP", medianDelta: medianDelta2, reason: `median +${medianDelta2} > +${t.keepThreshold}` };
|
|
4158
|
+
}
|
|
4159
|
+
if (medianDelta2 < -t.regressionThreshold) {
|
|
4160
|
+
return {
|
|
4161
|
+
verdict: "REGRESSION",
|
|
4162
|
+
medianDelta: medianDelta2,
|
|
4163
|
+
reason: `median ${medianDelta2} < -${t.regressionThreshold}`
|
|
4164
|
+
};
|
|
4165
|
+
}
|
|
4166
|
+
return {
|
|
4167
|
+
verdict: "NOISE",
|
|
4168
|
+
medianDelta: medianDelta2,
|
|
4169
|
+
reason: `median delta ${medianDelta2} inside noise band [-${t.regressionThreshold}, +${t.keepThreshold}]`
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
var gitProvenanceReader = () => {
|
|
4173
|
+
const run = (cmd) => execSync(cmd, { encoding: "utf8" }).trim();
|
|
4174
|
+
const commit = run("git rev-parse --short HEAD");
|
|
4175
|
+
const message = run("git log -1 --format=%s");
|
|
4176
|
+
const changedRaw = run("git diff --name-only HEAD~1");
|
|
4177
|
+
const changedFiles = changedRaw.length === 0 ? [] : changedRaw.split("\n").filter(Boolean);
|
|
4178
|
+
return { commit, message, changedFiles };
|
|
4179
|
+
};
|
|
4180
|
+
function inMemoryExperimentStore(initial = []) {
|
|
4181
|
+
let state = initial.map((e) => structuredClone(e));
|
|
4182
|
+
return {
|
|
4183
|
+
async load() {
|
|
4184
|
+
return state.map((e) => structuredClone(e));
|
|
4185
|
+
},
|
|
4186
|
+
async save(experiments) {
|
|
4187
|
+
state = experiments.map((e) => structuredClone(e));
|
|
4188
|
+
}
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
function fileExperimentStore(path) {
|
|
4192
|
+
return {
|
|
4193
|
+
async load() {
|
|
4194
|
+
const fs2 = await import("fs/promises");
|
|
4195
|
+
try {
|
|
4196
|
+
const raw = await fs2.readFile(path, "utf8");
|
|
4197
|
+
const parsed = JSON.parse(raw);
|
|
4198
|
+
if (!Array.isArray(parsed)) {
|
|
4199
|
+
throw new ValidationError(`experiment-tracker: store at ${path} is not a JSON array`);
|
|
4200
|
+
}
|
|
4201
|
+
return parsed;
|
|
4202
|
+
} catch (err) {
|
|
4203
|
+
if (err.code === "ENOENT") return [];
|
|
4204
|
+
throw err;
|
|
4205
|
+
}
|
|
4206
|
+
},
|
|
4207
|
+
async save(experiments) {
|
|
4208
|
+
const fs2 = await import("fs/promises");
|
|
4209
|
+
const pathMod = await import("path");
|
|
4210
|
+
await fs2.mkdir(pathMod.dirname(path), { recursive: true });
|
|
4211
|
+
await fs2.writeFile(path, JSON.stringify(experiments, null, 2), "utf8");
|
|
4212
|
+
}
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
4215
|
+
var ExperimentTracker = class {
|
|
4216
|
+
store;
|
|
4217
|
+
provenanceReader;
|
|
4218
|
+
thresholds;
|
|
4219
|
+
now;
|
|
4220
|
+
constructor(options = {}) {
|
|
4221
|
+
this.store = options.store ?? inMemoryExperimentStore();
|
|
4222
|
+
this.provenanceReader = options.provenanceReader ?? gitProvenanceReader;
|
|
4223
|
+
this.thresholds = resolveThresholds(options.thresholds);
|
|
4224
|
+
this.now = options.now ?? Date.now;
|
|
4225
|
+
}
|
|
4226
|
+
async create(input) {
|
|
4227
|
+
const experiments = await this.store.load();
|
|
4228
|
+
if (experiments.some((e) => e.id === input.id)) {
|
|
4229
|
+
throw new ValidationError(`experiment-tracker: experiment id "${input.id}" already exists`);
|
|
4230
|
+
}
|
|
4231
|
+
if (input.parentId && !experiments.some((e) => e.id === input.parentId)) {
|
|
4232
|
+
throw new ValidationError(
|
|
4233
|
+
`experiment-tracker: parent experiment "${input.parentId}" not found`
|
|
4234
|
+
);
|
|
4235
|
+
}
|
|
4236
|
+
const provenance = input.provenance ?? await this.provenanceReader();
|
|
4237
|
+
const experiment = {
|
|
4238
|
+
id: input.id,
|
|
4239
|
+
label: input.label,
|
|
4240
|
+
provenance,
|
|
4241
|
+
parentId: input.parentId,
|
|
4242
|
+
changeSummary: input.changeSummary,
|
|
4243
|
+
reps: [],
|
|
4244
|
+
stats: computeExperimentStats([], this.thresholds),
|
|
4245
|
+
verdict: "ITERATE",
|
|
4246
|
+
createdAt: new Date(this.now()).toISOString()
|
|
4247
|
+
};
|
|
4248
|
+
experiments.push(experiment);
|
|
4249
|
+
await this.store.save(experiments);
|
|
4250
|
+
return structuredClone(experiment);
|
|
4251
|
+
}
|
|
4252
|
+
/** Append a rep (its `rep` index defaults to the current rep count) and
|
|
4253
|
+
* recompute stats + verdict. Returns the updated experiment. */
|
|
4254
|
+
async addRep(experimentId, rep) {
|
|
4255
|
+
const experiments = await this.store.load();
|
|
4256
|
+
const exp = experiments.find((e) => e.id === experimentId);
|
|
4257
|
+
if (!exp)
|
|
4258
|
+
throw new ValidationError(`experiment-tracker: experiment "${experimentId}" not found`);
|
|
4259
|
+
const fullRep = {
|
|
4260
|
+
rep: rep.rep ?? exp.reps.length,
|
|
4261
|
+
score: rep.score,
|
|
4262
|
+
passed: rep.passed,
|
|
4263
|
+
metrics: rep.metrics,
|
|
4264
|
+
timestamp: rep.timestamp ?? new Date(this.now()).toISOString()
|
|
4265
|
+
};
|
|
4266
|
+
exp.reps.push(fullRep);
|
|
4267
|
+
exp.stats = computeExperimentStats(exp.reps, this.thresholds);
|
|
4268
|
+
const parent = exp.parentId ? experiments.find((e) => e.id === exp.parentId) : void 0;
|
|
4269
|
+
exp.verdict = improvementVerdict(exp.stats, parent?.stats ?? null, this.thresholds).verdict;
|
|
4270
|
+
await this.store.save(experiments);
|
|
4271
|
+
return structuredClone(exp);
|
|
4272
|
+
}
|
|
4273
|
+
async get(experimentId) {
|
|
4274
|
+
const experiments = await this.store.load();
|
|
4275
|
+
const found = experiments.find((e) => e.id === experimentId);
|
|
4276
|
+
return found ? structuredClone(found) : void 0;
|
|
4277
|
+
}
|
|
4278
|
+
async list() {
|
|
4279
|
+
return this.store.load();
|
|
4280
|
+
}
|
|
4281
|
+
/** Full verdict (not just the enum) for an experiment vs its parent. */
|
|
4282
|
+
async verdictFor(experimentId) {
|
|
4283
|
+
const experiments = await this.store.load();
|
|
4284
|
+
const exp = experiments.find((e) => e.id === experimentId);
|
|
4285
|
+
if (!exp)
|
|
4286
|
+
throw new ValidationError(`experiment-tracker: experiment "${experimentId}" not found`);
|
|
4287
|
+
const parent = exp.parentId ? experiments.find((e) => e.id === exp.parentId) : void 0;
|
|
4288
|
+
return improvementVerdict(exp.stats, parent?.stats ?? null, this.thresholds);
|
|
4289
|
+
}
|
|
4290
|
+
};
|
|
4291
|
+
|
|
3662
4292
|
// src/muffled-gate-scanner.ts
|
|
3663
4293
|
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
3664
4294
|
import { join } from "path";
|
|
@@ -3771,7 +4401,7 @@ var DEFAULT_FINDERS = [
|
|
|
3771
4401
|
];
|
|
3772
4402
|
var UNIVERSAL_FINDERS = [findConstructorCwdDropped];
|
|
3773
4403
|
function autoDeriveImporters(repoRoot, roots, extensions, importsContain) {
|
|
3774
|
-
const
|
|
4404
|
+
const matches2 = [];
|
|
3775
4405
|
const walk = (rel) => {
|
|
3776
4406
|
const abs = join(repoRoot, rel);
|
|
3777
4407
|
if (!existsSync(abs)) return;
|
|
@@ -3797,12 +4427,12 @@ function autoDeriveImporters(repoRoot, roots, extensions, importsContain) {
|
|
|
3797
4427
|
} catch {
|
|
3798
4428
|
continue;
|
|
3799
4429
|
}
|
|
3800
|
-
if (text.includes(importsContain))
|
|
4430
|
+
if (text.includes(importsContain)) matches2.push(sub);
|
|
3801
4431
|
}
|
|
3802
4432
|
}
|
|
3803
4433
|
};
|
|
3804
4434
|
for (const r of roots) walk(r);
|
|
3805
|
-
return
|
|
4435
|
+
return matches2;
|
|
3806
4436
|
}
|
|
3807
4437
|
function scanForMuffledGates(opts) {
|
|
3808
4438
|
const findings = [];
|
|
@@ -3974,16 +4604,80 @@ function isObject(v) {
|
|
|
3974
4604
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3975
4605
|
}
|
|
3976
4606
|
|
|
4607
|
+
// src/partition-held-out.ts
|
|
4608
|
+
function fnv1a32(input) {
|
|
4609
|
+
let h = 2166136261;
|
|
4610
|
+
for (let i = 0; i < input.length; i++) {
|
|
4611
|
+
h ^= input.charCodeAt(i) & 255;
|
|
4612
|
+
h = Math.imul(h, 16777619);
|
|
4613
|
+
}
|
|
4614
|
+
return h >>> 0;
|
|
4615
|
+
}
|
|
4616
|
+
function hashToUnit(id, seed) {
|
|
4617
|
+
return fnv1a32(`${id}
|
|
4618
|
+
${seed}`) / 4294967296;
|
|
4619
|
+
}
|
|
4620
|
+
function assignHeldOutTag(id, options = {}) {
|
|
4621
|
+
const seed = options.seed ?? "held-out-v1";
|
|
4622
|
+
const holdoutFraction = options.holdoutFraction ?? 0.5;
|
|
4623
|
+
assertFraction(holdoutFraction);
|
|
4624
|
+
return hashToUnit(id, seed) < holdoutFraction ? "holdout" : "search";
|
|
4625
|
+
}
|
|
4626
|
+
function partitionHeldOut(ids, options = {}) {
|
|
4627
|
+
const seed = options.seed ?? "held-out-v1";
|
|
4628
|
+
const holdoutFraction = options.holdoutFraction ?? 0.5;
|
|
4629
|
+
const minHoldout = options.minHoldout ?? 1;
|
|
4630
|
+
const minSearch = options.minSearch ?? 1;
|
|
4631
|
+
assertFraction(holdoutFraction);
|
|
4632
|
+
if (ids.length === 0) {
|
|
4633
|
+
throw new ValidationError("partitionHeldOut: no ids supplied");
|
|
4634
|
+
}
|
|
4635
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4636
|
+
const search = [];
|
|
4637
|
+
const holdout = [];
|
|
4638
|
+
for (const id of ids) {
|
|
4639
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
4640
|
+
throw new ValidationError(
|
|
4641
|
+
`partitionHeldOut: ids must be non-empty strings, got ${JSON.stringify(id)}`
|
|
4642
|
+
);
|
|
4643
|
+
}
|
|
4644
|
+
if (seen.has(id)) {
|
|
4645
|
+
throw new ValidationError(
|
|
4646
|
+
`partitionHeldOut: duplicate id "${id}" \u2014 a paired comparison must not double-count`
|
|
4647
|
+
);
|
|
4648
|
+
}
|
|
4649
|
+
seen.add(id);
|
|
4650
|
+
if (hashToUnit(id, seed) < holdoutFraction) holdout.push(id);
|
|
4651
|
+
else search.push(id);
|
|
4652
|
+
}
|
|
4653
|
+
if (holdout.length < minHoldout) {
|
|
4654
|
+
throw new ValidationError(
|
|
4655
|
+
`partitionHeldOut: holdout set has ${holdout.length} id(s), below the floor of ${minHoldout} (n=${ids.length}, holdoutFraction=${holdoutFraction}) \u2014 too few for a significant comparison`
|
|
4656
|
+
);
|
|
4657
|
+
}
|
|
4658
|
+
if (search.length < minSearch) {
|
|
4659
|
+
throw new ValidationError(
|
|
4660
|
+
`partitionHeldOut: search set has ${search.length} id(s), below the floor of ${minSearch} (n=${ids.length}, holdoutFraction=${holdoutFraction})`
|
|
4661
|
+
);
|
|
4662
|
+
}
|
|
4663
|
+
return { search, holdout, seed, holdoutFraction };
|
|
4664
|
+
}
|
|
4665
|
+
function assertFraction(f) {
|
|
4666
|
+
if (!Number.isFinite(f) || f <= 0 || f >= 1) {
|
|
4667
|
+
throw new ValidationError(`partitionHeldOut: holdoutFraction must be in (0, 1), got ${f}`);
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
|
|
3977
4671
|
// src/scorecard.ts
|
|
3978
4672
|
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "fs";
|
|
3979
4673
|
import { dirname } from "path";
|
|
3980
|
-
function
|
|
4674
|
+
function median2(xs) {
|
|
3981
4675
|
if (xs.length === 0) return 0;
|
|
3982
4676
|
const sorted = [...xs].sort((a, b) => a - b);
|
|
3983
4677
|
const mid = Math.floor(sorted.length / 2);
|
|
3984
4678
|
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
3985
4679
|
}
|
|
3986
|
-
function
|
|
4680
|
+
function runScore2(run) {
|
|
3987
4681
|
return run.outcome.holdoutScore ?? run.outcome.searchScore;
|
|
3988
4682
|
}
|
|
3989
4683
|
function aggregatePerDimension(runs) {
|
|
@@ -4017,14 +4711,14 @@ function recordRuns(runs, opts) {
|
|
|
4017
4711
|
}
|
|
4018
4712
|
const lines = [];
|
|
4019
4713
|
for (const [scenarioId, scenarioRuns] of byScenario) {
|
|
4020
|
-
const scored = scenarioRuns.map((run) => ({ run, score:
|
|
4714
|
+
const scored = scenarioRuns.map((run) => ({ run, score: runScore2(run) })).filter((s) => s.score !== void 0);
|
|
4021
4715
|
if (scored.length === 0) continue;
|
|
4022
4716
|
const scores2 = scored.map((s) => s.score);
|
|
4023
4717
|
const entry = {
|
|
4024
4718
|
commitSha: opts.commitSha,
|
|
4025
4719
|
timestamp,
|
|
4026
4720
|
scores: scores2,
|
|
4027
|
-
composite:
|
|
4721
|
+
composite: median2(scores2),
|
|
4028
4722
|
runIds: scored.map((s) => s.run.runId)
|
|
4029
4723
|
};
|
|
4030
4724
|
const perDimension = aggregatePerDimension(scenarioRuns);
|
|
@@ -5772,13 +6466,13 @@ function extractErrorCount(text, opts = {}) {
|
|
|
5772
6466
|
(p) => !opts.only || opts.only.includes(p.name)
|
|
5773
6467
|
);
|
|
5774
6468
|
for (const p of patterns) {
|
|
5775
|
-
const
|
|
5776
|
-
if (
|
|
5777
|
-
const count = p.transform ?
|
|
6469
|
+
const matches2 = Array.from(text.matchAll(p.regex));
|
|
6470
|
+
if (matches2.length === 0) continue;
|
|
6471
|
+
const count = p.transform ? matches2.reduce((sum3, m) => sum3 + p.transform(m), 0) : matches2.length;
|
|
5778
6472
|
return {
|
|
5779
6473
|
count,
|
|
5780
6474
|
matched: p.name,
|
|
5781
|
-
samples:
|
|
6475
|
+
samples: matches2.slice(0, 5).map((m) => m[0])
|
|
5782
6476
|
};
|
|
5783
6477
|
}
|
|
5784
6478
|
return { count: null, matched: null, samples: [] };
|
|
@@ -6719,7 +7413,7 @@ function scoreScenario(scenario, matcher, threshold, matchStrategy) {
|
|
|
6719
7413
|
}
|
|
6720
7414
|
function scoreScenarioReferenceOrder(scenario, matcher, threshold) {
|
|
6721
7415
|
const candidatesLeft = scenario.candidates.map((candidate, index) => ({ candidate, index }));
|
|
6722
|
-
const
|
|
7416
|
+
const matches2 = [];
|
|
6723
7417
|
for (const reference of scenario.references) {
|
|
6724
7418
|
let best = null;
|
|
6725
7419
|
for (const item of candidatesLeft) {
|
|
@@ -6732,7 +7426,7 @@ function scoreScenarioReferenceOrder(scenario, matcher, threshold) {
|
|
|
6732
7426
|
if (best && best.score >= threshold) {
|
|
6733
7427
|
const matchIndex = candidatesLeft.findIndex((item) => item.index === best.index);
|
|
6734
7428
|
if (matchIndex >= 0) candidatesLeft.splice(matchIndex, 1);
|
|
6735
|
-
|
|
7429
|
+
matches2.push({
|
|
6736
7430
|
scenarioId: scenario.id,
|
|
6737
7431
|
referenceId: reference.id,
|
|
6738
7432
|
candidateId: best.candidate.id,
|
|
@@ -6742,7 +7436,7 @@ function scoreScenarioReferenceOrder(scenario, matcher, threshold) {
|
|
|
6742
7436
|
reason: best.reason
|
|
6743
7437
|
});
|
|
6744
7438
|
} else {
|
|
6745
|
-
|
|
7439
|
+
matches2.push({
|
|
6746
7440
|
scenarioId: scenario.id,
|
|
6747
7441
|
referenceId: reference.id,
|
|
6748
7442
|
candidateId: best?.candidate.id ?? null,
|
|
@@ -6753,7 +7447,7 @@ function scoreScenarioReferenceOrder(scenario, matcher, threshold) {
|
|
|
6753
7447
|
});
|
|
6754
7448
|
}
|
|
6755
7449
|
}
|
|
6756
|
-
return buildScenarioScore(scenario,
|
|
7450
|
+
return buildScenarioScore(scenario, matches2, candidatesLeft.length);
|
|
6757
7451
|
}
|
|
6758
7452
|
function scoreScenarioGlobalGreedy(scenario, matcher, threshold) {
|
|
6759
7453
|
const pairs = [];
|
|
@@ -6780,7 +7474,7 @@ function scoreScenarioGlobalGreedy(scenario, matcher, threshold) {
|
|
|
6780
7474
|
selectedByReference.set(pair.referenceIndex, pair);
|
|
6781
7475
|
selectedCandidates.add(pair.candidateIndex);
|
|
6782
7476
|
}
|
|
6783
|
-
const
|
|
7477
|
+
const matches2 = scenario.references.map((reference, referenceIndex) => {
|
|
6784
7478
|
const weight = reference.weight ?? 1;
|
|
6785
7479
|
const selected = selectedByReference.get(referenceIndex);
|
|
6786
7480
|
if (selected) {
|
|
@@ -6805,7 +7499,7 @@ function scoreScenarioGlobalGreedy(scenario, matcher, threshold) {
|
|
|
6805
7499
|
reason: bestRejected?.reason ?? "no candidates"
|
|
6806
7500
|
};
|
|
6807
7501
|
});
|
|
6808
|
-
return buildScenarioScore(scenario,
|
|
7502
|
+
return buildScenarioScore(scenario, matches2, scenario.candidates.length - selectedCandidates.size);
|
|
6809
7503
|
}
|
|
6810
7504
|
function scorePair(scenario, matcher, reference, candidate) {
|
|
6811
7505
|
const result = matcher(reference, candidate, scenario);
|
|
@@ -6816,11 +7510,11 @@ function scorePair(scenario, matcher, reference, candidate) {
|
|
|
6816
7510
|
}
|
|
6817
7511
|
return { score: clamp012(result.score), reason: result.reason ?? "" };
|
|
6818
7512
|
}
|
|
6819
|
-
function buildScenarioScore(scenario,
|
|
6820
|
-
const matched =
|
|
7513
|
+
function buildScenarioScore(scenario, matches2, falsePositives) {
|
|
7514
|
+
const matched = matches2.filter((match) => match.matched).length;
|
|
6821
7515
|
const total = scenario.references.length;
|
|
6822
|
-
const matchedWeight =
|
|
6823
|
-
const totalWeight =
|
|
7516
|
+
const matchedWeight = matches2.filter((match) => match.matched).reduce((sum3, match) => sum3 + match.weight, 0);
|
|
7517
|
+
const totalWeight = matches2.reduce((sum3, match) => sum3 + match.weight, 0);
|
|
6824
7518
|
const precision2 = ratio(matched, matched + falsePositives);
|
|
6825
7519
|
const recall = ratio(matched, total);
|
|
6826
7520
|
return {
|
|
@@ -6834,7 +7528,7 @@ function buildScenarioScore(scenario, matches, falsePositives) {
|
|
|
6834
7528
|
precision: precision2,
|
|
6835
7529
|
recall,
|
|
6836
7530
|
f1: f1(precision2, recall),
|
|
6837
|
-
matches
|
|
7531
|
+
matches: matches2
|
|
6838
7532
|
};
|
|
6839
7533
|
}
|
|
6840
7534
|
function aggregateBySplit(scores2) {
|
|
@@ -7095,7 +7789,7 @@ function createDefaultReviewer(options) {
|
|
|
7095
7789
|
|
|
7096
7790
|
// src/description-length-gate.ts
|
|
7097
7791
|
import { gzipSync } from "zlib";
|
|
7098
|
-
function
|
|
7792
|
+
function runScore3(run) {
|
|
7099
7793
|
const o = run.outcome;
|
|
7100
7794
|
const s = o.holdoutScore ?? o.searchScore ?? o.raw?.score;
|
|
7101
7795
|
return typeof s === "number" && Number.isFinite(s) ? s : void 0;
|
|
@@ -7106,7 +7800,7 @@ function taskKey(run) {
|
|
|
7106
7800
|
function perTaskMeanScore(runs) {
|
|
7107
7801
|
const acc = /* @__PURE__ */ new Map();
|
|
7108
7802
|
for (const run of runs) {
|
|
7109
|
-
const s =
|
|
7803
|
+
const s = runScore3(run);
|
|
7110
7804
|
if (s === void 0) continue;
|
|
7111
7805
|
const key = taskKey(run);
|
|
7112
7806
|
const cur = acc.get(key) ?? { sum: 0, n: 0 };
|
|
@@ -7258,10 +7952,10 @@ async function discoverPersonas(dir, opts = {}) {
|
|
|
7258
7952
|
function matchGoldens(goldens, candidates, options = {}) {
|
|
7259
7953
|
const extract = options.text ?? defaultExtract2;
|
|
7260
7954
|
const haystacks = candidates.map((c) => extract(c).toLowerCase());
|
|
7261
|
-
const
|
|
7955
|
+
const matches2 = goldens.map((golden) => goldenMatched(golden, haystacks));
|
|
7262
7956
|
return {
|
|
7263
|
-
matches,
|
|
7264
|
-
hits:
|
|
7957
|
+
matches: matches2,
|
|
7958
|
+
hits: matches2.filter(Boolean).length,
|
|
7265
7959
|
total: goldens.length
|
|
7266
7960
|
};
|
|
7267
7961
|
}
|
|
@@ -8416,6 +9110,7 @@ export {
|
|
|
8416
9110
|
CaptureIntegrityError,
|
|
8417
9111
|
ConfigError,
|
|
8418
9112
|
ConvergenceTracker,
|
|
9113
|
+
CostLedger,
|
|
8419
9114
|
CostTracker,
|
|
8420
9115
|
CrossFamilyError,
|
|
8421
9116
|
DEFAULT_AGENT_SLOS,
|
|
@@ -8437,6 +9132,8 @@ export {
|
|
|
8437
9132
|
DockerSandboxDriver,
|
|
8438
9133
|
DualAgentBench,
|
|
8439
9134
|
ERROR_COUNT_PATTERNS,
|
|
9135
|
+
EvalTraceStore,
|
|
9136
|
+
ExperimentTracker,
|
|
8440
9137
|
FAILURE_CLASSES,
|
|
8441
9138
|
FAILURE_MODE_KIND_SPEC,
|
|
8442
9139
|
FileSystemFeedbackTrajectoryStore,
|
|
@@ -8462,6 +9159,7 @@ export {
|
|
|
8462
9159
|
LockedJsonlAppender,
|
|
8463
9160
|
MODEL_PRICING,
|
|
8464
9161
|
MetricsCollector,
|
|
9162
|
+
ModelsUnreachableError,
|
|
8465
9163
|
MultiLayerVerifier,
|
|
8466
9164
|
Mutex,
|
|
8467
9165
|
NoopRawProviderSink,
|
|
@@ -8521,12 +9219,14 @@ export {
|
|
|
8521
9219
|
asString,
|
|
8522
9220
|
assertCrossFamily,
|
|
8523
9221
|
assertLlmRoute,
|
|
9222
|
+
assertModelsServed,
|
|
8524
9223
|
assertRealBackend,
|
|
8525
9224
|
assertReleaseConfidence,
|
|
8526
9225
|
assertRunAgentProfileCell,
|
|
8527
9226
|
assertRunCaptured,
|
|
8528
9227
|
assertSingleBackend,
|
|
8529
9228
|
assignFeedbackSplit,
|
|
9229
|
+
assignHeldOutTag,
|
|
8530
9230
|
attributeCounterfactuals,
|
|
8531
9231
|
backoffMs,
|
|
8532
9232
|
deterministicSplit as benchmarkDeterministicSplit,
|
|
@@ -8574,6 +9274,7 @@ export {
|
|
|
8574
9274
|
compilerJudge,
|
|
8575
9275
|
composeParsers,
|
|
8576
9276
|
composeValidators,
|
|
9277
|
+
computeExperimentStats,
|
|
8577
9278
|
computeFindingId,
|
|
8578
9279
|
computeToolUseMetrics,
|
|
8579
9280
|
computeTraceMetrics,
|
|
@@ -8586,6 +9287,7 @@ export {
|
|
|
8586
9287
|
convertTraceStoresToOtlp,
|
|
8587
9288
|
corpusInterRaterAgreement,
|
|
8588
9289
|
corpusInterRaterAgreementFromJudgeScores,
|
|
9290
|
+
costForUsage,
|
|
8589
9291
|
createAnalystAi,
|
|
8590
9292
|
createAntiSlopJudge,
|
|
8591
9293
|
createChatClient,
|
|
@@ -8641,6 +9343,9 @@ export {
|
|
|
8641
9343
|
extractErrorCount,
|
|
8642
9344
|
extractOtlpAttributes,
|
|
8643
9345
|
extractProducedState,
|
|
9346
|
+
extractUsage,
|
|
9347
|
+
extractUsageFromResponse,
|
|
9348
|
+
extractUsageFromSse,
|
|
8644
9349
|
feedbackTrajectoriesToDatasetScenarios,
|
|
8645
9350
|
feedbackTrajectoriesToOptimizerRows,
|
|
8646
9351
|
feedbackTrajectoryToDatasetScenario,
|
|
@@ -8648,6 +9353,7 @@ export {
|
|
|
8648
9353
|
fieldAgreement,
|
|
8649
9354
|
fileContains,
|
|
8650
9355
|
fileExists,
|
|
9356
|
+
fileExperimentStore,
|
|
8651
9357
|
findAutoMatchNoExpectation,
|
|
8652
9358
|
findConstructorCwdDropped,
|
|
8653
9359
|
findFallbackToPass,
|
|
@@ -8657,12 +9363,14 @@ export {
|
|
|
8657
9363
|
firstStringAttr,
|
|
8658
9364
|
flattenOtlpExportToNdjson,
|
|
8659
9365
|
flowLayer,
|
|
9366
|
+
fnv1a32,
|
|
8660
9367
|
formatBenchmarkReport,
|
|
8661
9368
|
formatDriverReport,
|
|
8662
9369
|
formatFindings,
|
|
8663
9370
|
formatScorecardDiff,
|
|
8664
9371
|
gainHistogram,
|
|
8665
9372
|
ghCliClient,
|
|
9373
|
+
gitProvenanceReader,
|
|
8666
9374
|
precision as goldenPrecision,
|
|
8667
9375
|
gradeSemanticStatus,
|
|
8668
9376
|
groupBy,
|
|
@@ -8670,10 +9378,14 @@ export {
|
|
|
8670
9378
|
hashContent,
|
|
8671
9379
|
hashJson,
|
|
8672
9380
|
hashScenarios,
|
|
9381
|
+
hashToUnit,
|
|
8673
9382
|
htmlContainsElement,
|
|
8674
9383
|
httpGithubClient,
|
|
9384
|
+
improvementVerdict,
|
|
9385
|
+
inMemoryExperimentStore,
|
|
8675
9386
|
inMemoryReferenceReplayStore,
|
|
8676
9387
|
inMemoryReviewStore,
|
|
9388
|
+
inMemoryRunRecordBackend,
|
|
8677
9389
|
inferDomainKeywords,
|
|
8678
9390
|
inferOtlpKind,
|
|
8679
9391
|
interRaterReliability,
|
|
@@ -8694,6 +9406,7 @@ export {
|
|
|
8694
9406
|
jsonShape,
|
|
8695
9407
|
jsonlReferenceReplayStore,
|
|
8696
9408
|
jsonlReviewStore,
|
|
9409
|
+
jsonlRunRecordBackend,
|
|
8697
9410
|
judgeFamily,
|
|
8698
9411
|
judgeReplayGate,
|
|
8699
9412
|
judgeSpans,
|
|
@@ -8713,6 +9426,7 @@ export {
|
|
|
8713
9426
|
mergeLayerResults,
|
|
8714
9427
|
mergeSteeringBundle,
|
|
8715
9428
|
modelDescriptionBits,
|
|
9429
|
+
modelPriceKey,
|
|
8716
9430
|
multiToolchainLayer,
|
|
8717
9431
|
nistAiRmfReport,
|
|
8718
9432
|
normalizeScores,
|
|
@@ -8737,11 +9451,13 @@ export {
|
|
|
8737
9451
|
parseRunRecordSafe,
|
|
8738
9452
|
parseRuntimeTrajectoryHookEvent,
|
|
8739
9453
|
partialCredit,
|
|
9454
|
+
partitionHeldOut,
|
|
8740
9455
|
passOrthogonality,
|
|
8741
9456
|
pixelDeltaRatio,
|
|
8742
9457
|
planTraceInsightQuestions,
|
|
8743
9458
|
politenessPrefixMutator,
|
|
8744
9459
|
positionalBias,
|
|
9460
|
+
preflightModels,
|
|
8745
9461
|
printDriverSummary,
|
|
8746
9462
|
probeLlm,
|
|
8747
9463
|
profile_exports as profile,
|
|
@@ -8801,6 +9517,7 @@ export {
|
|
|
8801
9517
|
runProposeReview,
|
|
8802
9518
|
runProposeReviewAsControlLoop,
|
|
8803
9519
|
runReferenceReplay,
|
|
9520
|
+
runScore,
|
|
8804
9521
|
runSelfPlay,
|
|
8805
9522
|
runSemanticConceptJudge,
|
|
8806
9523
|
runTestGradedScenario,
|