auto-model-router 0.30.3 → 0.31.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/.omp-plugin/marketplace.json +2 -2
- package/package.json +1 -1
- package/src/eval/calibrate.ts +47 -12
- package/src/eval/run.ts +18 -2
- package/src/server/http.ts +8 -5
- package/test/eval.test.ts +62 -5
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "auto-model-router: a local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.31.0",
|
|
11
11
|
"pluginRoot": "."
|
|
12
12
|
},
|
|
13
13
|
"plugins": [
|
|
14
14
|
{
|
|
15
15
|
"name": "auto-model-router",
|
|
16
16
|
"description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter. Runs in-process, routes per turn by price and task complexity, with budget caps, mid-stream escalation, and cache-aware hysteresis.",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.31.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "drewappling",
|
|
20
20
|
"email": "drewappling@gmail.com"
|
package/package.json
CHANGED
package/src/eval/calibrate.ts
CHANGED
|
@@ -58,13 +58,26 @@ export interface AnchorPoint {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* The raw range the anchors must actually cover before a line through them means anything.
|
|
62
|
+
*
|
|
63
|
+
* Correlation alone does NOT catch a compressed fit: three anchors published 20/50/80 that
|
|
64
|
+
* our suite scores 0.96/0.97/0.98 correlate at r = 1.0, and the line they define has a slope
|
|
65
|
+
* of ~3000 index points per unit of raw score. That fit is arithmetically perfect and
|
|
66
|
+
* completely useless — it is how a model published at 39.5 was calibrated to 22.8. If the
|
|
67
|
+
* anchors barely differ on our suite, our suite cannot place anything between them.
|
|
68
|
+
*/
|
|
69
|
+
export const MIN_RAW_SPREAD = 0.15;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* OLS fit, or null when the calibration cannot be trusted: too few points, too narrow a raw
|
|
73
|
+
* range, no spread, a non-positive slope, or weak correlation. A suite that does not track
|
|
74
|
+
* AA positively, with real correlation, over a real range would turn a target's score into
|
|
75
|
+
* noise dressed as signal, so we refuse it and emit nothing for that axis.
|
|
65
76
|
*/
|
|
66
77
|
export function fitAxis(points: readonly AnchorPoint[]): LineFit | null {
|
|
67
78
|
if (points.length < MIN_ANCHORS) return null;
|
|
79
|
+
const raws = points.map((p) => p.raw);
|
|
80
|
+
if (Math.max(...raws) - Math.min(...raws) < MIN_RAW_SPREAD) return null;
|
|
68
81
|
const n = points.length;
|
|
69
82
|
let sx = 0;
|
|
70
83
|
let sy = 0;
|
|
@@ -101,19 +114,32 @@ export function applyFit(fit: LineFit, raw: number): number {
|
|
|
101
114
|
const AXES: readonly QualityAxis[] = ["coding", "intelligence", "agentic"];
|
|
102
115
|
|
|
103
116
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
117
|
+
* The correlation a fit must reach before its numbers are published as scores. `MIN_R` (0.5)
|
|
118
|
+
* is the bar for a fit being computable at all; this is the bar for TRUSTING one. The fit's
|
|
119
|
+
* `r` and `n` were previously computed and then discarded, so an r of 0.51 and one of 0.99
|
|
120
|
+
* produced indistinguishable output — and a shallow fit silently compressed every target.
|
|
121
|
+
*/
|
|
122
|
+
export const PUBLISH_MIN_R = 0.8;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Fit every axis from the anchors' raw suite scores paired with their known AA scores.
|
|
126
|
+
* `anchorAa` supplies the AA index per slug+axis (absent ⇒ that anchor is not used there).
|
|
127
|
+
*
|
|
128
|
+
* `rawOf` selects which observations the fit is built from. It defaults to every band, but
|
|
129
|
+
* callers should pass the HARD band: easy and moderate sit at ~1.0 for every model worth
|
|
130
|
+
* ranking, so including them leaves the regression almost no variation in x against a wide
|
|
131
|
+
* spread in published y, and the slope collapses toward flat.
|
|
107
132
|
*/
|
|
108
133
|
export function fitCalibration(
|
|
109
134
|
anchors: readonly EvalResult[],
|
|
110
135
|
anchorAa: (slug: string, axis: QualityAxis) => number | undefined,
|
|
136
|
+
rawOf: (result: EvalResult, axis: QualityAxis) => number | null = (r, axis) => axisMean(r.axes[axis]),
|
|
111
137
|
): Calibration {
|
|
112
138
|
const cal: Calibration = {};
|
|
113
139
|
for (const axis of AXES) {
|
|
114
140
|
const points: AnchorPoint[] = [];
|
|
115
141
|
for (const r of anchors) {
|
|
116
|
-
const raw =
|
|
142
|
+
const raw = rawOf(r, axis);
|
|
117
143
|
const aa = anchorAa(r.slug, axis);
|
|
118
144
|
if (raw !== null && aa !== undefined) points.push({ raw, aa });
|
|
119
145
|
}
|
|
@@ -123,15 +149,24 @@ export function fitCalibration(
|
|
|
123
149
|
return cal;
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
function axisMean(a: AxisScore | undefined): number | null {
|
|
152
|
+
export function axisMean(a: AxisScore | undefined): number | null {
|
|
127
153
|
return a === undefined || a.n === 0 ? null : a.sum / a.n;
|
|
128
154
|
}
|
|
129
155
|
|
|
130
|
-
/**
|
|
156
|
+
/** The hard band alone, for calibration. */
|
|
157
|
+
export const hardRaw = (r: EvalResult, axis: QualityAxis): number | null => axisMean(r.axesHard[axis]);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Calibrated local FeedScores for the targets, one axis at a time. An axis is skipped when it
|
|
161
|
+
* has no fit, when the fit is weaker than `minR`, or when the target made no observation on
|
|
162
|
+
* it — honest silence rather than a number nobody should act on.
|
|
163
|
+
*/
|
|
131
164
|
export function toLocalFeedScores(
|
|
132
165
|
targets: readonly EvalResult[],
|
|
133
166
|
cal: Calibration,
|
|
134
167
|
authorOf: (slug: string) => string,
|
|
168
|
+
rawOf: (result: EvalResult, axis: QualityAxis) => number | null = (r, axis) => axisMean(r.axes[axis]),
|
|
169
|
+
minR = PUBLISH_MIN_R,
|
|
135
170
|
): FeedScore[] {
|
|
136
171
|
const out: FeedScore[] = [];
|
|
137
172
|
for (const r of targets) {
|
|
@@ -139,8 +174,8 @@ export function toLocalFeedScores(
|
|
|
139
174
|
let any = false;
|
|
140
175
|
for (const axis of AXES) {
|
|
141
176
|
const fit = cal[axis];
|
|
142
|
-
const raw =
|
|
143
|
-
if (fit === undefined || raw === null) continue;
|
|
177
|
+
const raw = rawOf(r, axis);
|
|
178
|
+
if (fit === undefined || raw === null || fit.r < minR) continue;
|
|
144
179
|
entry[axis] = applyFit(fit, raw);
|
|
145
180
|
any = true;
|
|
146
181
|
}
|
package/src/eval/run.ts
CHANGED
|
@@ -39,6 +39,14 @@ export interface EvalResult {
|
|
|
39
39
|
* questions were easy.
|
|
40
40
|
*/
|
|
41
41
|
byComplexity: Partial<Record<Complexity, AxisScore>>;
|
|
42
|
+
/**
|
|
43
|
+
* Per-axis scores from the HARD band alone. Calibration fits against these: the easy and
|
|
44
|
+
* moderate bands sit at ~1.0 for every model worth ranking, so including them gives the
|
|
45
|
+
* regression almost no variation in x against a wide spread in published y — the fitted
|
|
46
|
+
* slope goes shallow and every target is dragged toward the middle. Measured: a model
|
|
47
|
+
* published at intelligence 39.5 calibrated to 22.8 across 10 passes.
|
|
48
|
+
*/
|
|
49
|
+
axesHard: Record<QualityAxis, AxisScore>;
|
|
42
50
|
/**
|
|
43
51
|
* Per-axis spread across passes: max pass mean minus min pass mean, or null under two
|
|
44
52
|
* passes. A wide spread means the headline is one sample of a noisy quantity, and is the
|
|
@@ -144,6 +152,7 @@ async function scorePass(slug: string, args: RunEvalArgs): Promise<EvalResult> {
|
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
const byComplexity: Partial<Record<Complexity, AxisScore>> = {};
|
|
155
|
+
const axesHard: Record<QualityAxis, AxisScore> = { coding: { sum: 0, n: 0 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } };
|
|
147
156
|
for (const o of [...objective, ...judgedOutcomes, ...scenarioOutcomes]) {
|
|
148
157
|
if (!o.ok) {
|
|
149
158
|
// An unobserved task is NOT a zero: a provider's throttle or outage would otherwise
|
|
@@ -154,11 +163,15 @@ async function scorePass(slug: string, args: RunEvalArgs): Promise<EvalResult> {
|
|
|
154
163
|
}
|
|
155
164
|
axes[o.axis].sum += o.grade;
|
|
156
165
|
axes[o.axis].n += 1;
|
|
166
|
+
if (o.complexity === "hard") {
|
|
167
|
+
axesHard[o.axis].sum += o.grade;
|
|
168
|
+
axesHard[o.axis].n += 1;
|
|
169
|
+
}
|
|
157
170
|
const band = (byComplexity[o.complexity] ??= { sum: 0, n: 0 });
|
|
158
171
|
band.sum += o.grade;
|
|
159
172
|
band.n += 1;
|
|
160
173
|
}
|
|
161
|
-
return { slug, axes, errors, repeats: 1, spread: {}, byComplexity };
|
|
174
|
+
return { slug, axes, errors, repeats: 1, spread: {}, byComplexity, axesHard };
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
const AXES: readonly QualityAxis[] = ["coding", "intelligence", "agentic"];
|
|
@@ -174,6 +187,7 @@ async function scoreModel(slug: string, args: RunEvalArgs): Promise<EvalResult>
|
|
|
174
187
|
const axes: Record<QualityAxis, AxisScore> = { coding: { sum: 0, n: 0 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } };
|
|
175
188
|
const means: Record<QualityAxis, number[]> = { coding: [], intelligence: [], agentic: [] };
|
|
176
189
|
const byComplexity: Partial<Record<Complexity, AxisScore>> = {};
|
|
190
|
+
const axesHard: Record<QualityAxis, AxisScore> = { coding: { sum: 0, n: 0 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } };
|
|
177
191
|
let errors = 0;
|
|
178
192
|
for (let i = 0; i < passes; i++) {
|
|
179
193
|
const pass = await scorePass(slug, args);
|
|
@@ -182,6 +196,8 @@ async function scoreModel(slug: string, args: RunEvalArgs): Promise<EvalResult>
|
|
|
182
196
|
for (const axis of AXES) {
|
|
183
197
|
axes[axis].sum += pass.axes[axis].sum;
|
|
184
198
|
axes[axis].n += pass.axes[axis].n;
|
|
199
|
+
axesHard[axis].sum += pass.axesHard[axis].sum;
|
|
200
|
+
axesHard[axis].n += pass.axesHard[axis].n;
|
|
185
201
|
if (pass.axes[axis].n > 0) means[axis].push(pass.axes[axis].sum / pass.axes[axis].n);
|
|
186
202
|
}
|
|
187
203
|
for (const [band, score] of Object.entries(pass.byComplexity) as [Complexity, AxisScore][]) {
|
|
@@ -195,7 +211,7 @@ async function scoreModel(slug: string, args: RunEvalArgs): Promise<EvalResult>
|
|
|
195
211
|
const m = means[axis];
|
|
196
212
|
if (m.length > 1) spread[axis] = Math.max(...m) - Math.min(...m);
|
|
197
213
|
}
|
|
198
|
-
return { slug, axes, errors, repeats: passes, spread, byComplexity };
|
|
214
|
+
return { slug, axes, errors, repeats: passes, spread, byComplexity, axesHard };
|
|
199
215
|
}
|
|
200
216
|
|
|
201
217
|
export async function runEval(args: RunEvalArgs): Promise<EvalResult[]> {
|
package/src/server/http.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { runEval, type Completer } from "../eval/run.ts";
|
|
|
18
18
|
import type { ToolSpec } from "../eval/agentic.ts";
|
|
19
19
|
import type { ToolCall } from "../upstream/types.ts";
|
|
20
20
|
import type { QualityAxis } from "../config/types.ts";
|
|
21
|
-
import { fitCalibration, pickAnchors, toLocalFeedScores, MIN_ANCHORS } from "../eval/calibrate.ts";
|
|
21
|
+
import { fitCalibration, hardRaw, pickAnchors, toLocalFeedScores, MIN_ANCHORS, PUBLISH_MIN_R } from "../eval/calibrate.ts";
|
|
22
22
|
import { makeJudge } from "../eval/judge.ts";
|
|
23
23
|
import { loadLocalScores, saveLocalScores } from "../catalog/benchmark-feeds.ts";
|
|
24
24
|
import { advise } from "./advise.ts";
|
|
@@ -812,12 +812,15 @@ export function startServer(cfg: RouterConfig): StartedServer {
|
|
|
812
812
|
});
|
|
813
813
|
const target = results[0]!;
|
|
814
814
|
const published = (s: string, axis: QualityAxis): number | undefined => models.find((m) => m.slug === s)?.quality[axis];
|
|
815
|
-
const cal = fitCalibration(results.slice(1), published);
|
|
815
|
+
const cal = fitCalibration(results.slice(1), published, hardRaw);
|
|
816
816
|
const authorOf = (s: string): string => models.find((m) => m.slug === s)?.author ?? "";
|
|
817
|
-
const fresh = toLocalFeedScores([target], cal, authorOf);
|
|
818
|
-
|
|
817
|
+
const fresh = toLocalFeedScores([target], cal, authorOf, hardRaw);
|
|
818
|
+
// The fit is reported, not just used: `r` and `n` say whether the numbers deserve
|
|
819
|
+
// belief, and an axis dropped for a weak fit should say so rather than vanish.
|
|
820
|
+
const fitDetail = Object.fromEntries(Object.entries(cal).map(([axis, f]) => [axis, { r: Number(f.r.toFixed(3)), n: f.n, published: f.r >= PUBLISH_MIN_R }]));
|
|
821
|
+
const shared = { slug, anchors, raw: target.axes, rawHard: target.axesHard, byComplexity: target.byComplexity, repeats: target.repeats, spread: target.spread, errors: target.errors, fit: fitDetail, publishMinR: PUBLISH_MIN_R, tookMs: Date.now() - started };
|
|
819
822
|
if (fresh.length === 0) {
|
|
820
|
-
benchmarkJobs.set(jobId, { ...benchmarkJobs.get(jobId)!, state: "done", result: { ...shared, calibrated: null, applied: false, reason:
|
|
823
|
+
benchmarkJobs.set(jobId, { ...benchmarkJobs.get(jobId)!, state: "done", result: { ...shared, calibrated: null, applied: false, reason: `no axis produced a fit at r >= ${PUBLISH_MIN_R} on the hard band; try more or better-spread anchors` } });
|
|
821
824
|
return;
|
|
822
825
|
}
|
|
823
826
|
// Merge, never replace: other models' measurements are not this run's to discard.
|
package/test/eval.test.ts
CHANGED
|
@@ -3,9 +3,10 @@ import { describe, expect, test } from "bun:test";
|
|
|
3
3
|
import { normalizeCatalogModel } from "../src/catalog/openrouter-catalog.ts";
|
|
4
4
|
import { applyFeedScores, loadLocalScores, saveLocalScores, type FeedScore } from "../src/catalog/benchmark-feeds.ts";
|
|
5
5
|
import { answerScore, extractJson, isRefusalOrEmpty, jsonField, tokenCoverage } from "../src/eval/grade.ts";
|
|
6
|
-
import { applyFit, fitAxis, fitCalibration, pickAnchors, toLocalFeedScores, MIN_ANCHORS } from "../src/eval/calibrate.ts";
|
|
6
|
+
import { applyFit, fitAxis, fitCalibration, hardRaw, pickAnchors, toLocalFeedScores, MIN_ANCHORS, MIN_R, PUBLISH_MIN_R } from "../src/eval/calibrate.ts";
|
|
7
7
|
import { runEval, type EvalResult } from "../src/eval/run.ts";
|
|
8
8
|
import { EVAL_TASKS } from "../src/eval/tasks.ts";
|
|
9
|
+
import type { QualityAxis } from "../src/config/types.ts";
|
|
9
10
|
import { makeJudge, parseScore } from "../src/eval/judge.ts";
|
|
10
11
|
import type { EvalTask, JudgedTask } from "../src/eval/tasks.ts";
|
|
11
12
|
import { openDb } from "../src/util/sqlite.ts";
|
|
@@ -130,9 +131,9 @@ describe("calibration", () => {
|
|
|
130
131
|
test("fitCalibration + toLocalFeedScores place a target on the AA scale", () => {
|
|
131
132
|
expect(MIN_ANCHORS).toBe(3);
|
|
132
133
|
const anchors: EvalResult[] = [
|
|
133
|
-
{ slug: "a/one", axes: { coding: { sum: 0.2, n: 1 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } }, errors: 0, repeats: 1, spread: {}, byComplexity: {} },
|
|
134
|
-
{ slug: "a/two", axes: { coding: { sum: 0.5, n: 1 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } }, errors: 0, repeats: 1, spread: {}, byComplexity: {} },
|
|
135
|
-
{ slug: "a/three", axes: { coding: { sum: 0.8, n: 1 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } }, errors: 0, repeats: 1, spread: {}, byComplexity: {} },
|
|
134
|
+
{ slug: "a/one", axes: { coding: { sum: 0.2, n: 1 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } }, errors: 0, repeats: 1, spread: {}, byComplexity: {}, axesHard: { coding: { sum: 0, n: 0 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } } },
|
|
135
|
+
{ slug: "a/two", axes: { coding: { sum: 0.5, n: 1 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } }, errors: 0, repeats: 1, spread: {}, byComplexity: {}, axesHard: { coding: { sum: 0, n: 0 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } } },
|
|
136
|
+
{ slug: "a/three", axes: { coding: { sum: 0.8, n: 1 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } }, errors: 0, repeats: 1, spread: {}, byComplexity: {}, axesHard: { coding: { sum: 0, n: 0 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } } },
|
|
136
137
|
];
|
|
137
138
|
const aaOf: Record<string, number> = { "a/one": 40, "a/two": 60, "a/three": 80 };
|
|
138
139
|
const cal = fitCalibration(anchors, (slug, axis) => (axis === "coding" ? aaOf[slug] : undefined));
|
|
@@ -140,7 +141,7 @@ describe("calibration", () => {
|
|
|
140
141
|
expect(cal.intelligence).toBeUndefined(); // no anchor data on that axis
|
|
141
142
|
|
|
142
143
|
const targets: EvalResult[] = [
|
|
143
|
-
{ slug: "z/gap", axes: { coding: { sum: 0.5, n: 1 }, intelligence: { sum: 0.9, n: 1 }, agentic: { sum: 0, n: 0 } }, errors: 0, repeats: 1, spread: {}, byComplexity: {} },
|
|
144
|
+
{ slug: "z/gap", axes: { coding: { sum: 0.5, n: 1 }, intelligence: { sum: 0.9, n: 1 }, agentic: { sum: 0, n: 0 } }, errors: 0, repeats: 1, spread: {}, byComplexity: {}, axesHard: { coding: { sum: 0, n: 0 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } } },
|
|
144
145
|
];
|
|
145
146
|
const local = toLocalFeedScores(targets, cal, (s) => s.slice(0, s.indexOf("/")));
|
|
146
147
|
expect(local).toHaveLength(1);
|
|
@@ -148,6 +149,62 @@ describe("calibration", () => {
|
|
|
148
149
|
expect(local[0]!.coding).toBeCloseTo(60, 5); // calibrated from raw 0.5
|
|
149
150
|
expect(local[0]!.intelligence).toBeUndefined(); // axis had no fit, so not emitted
|
|
150
151
|
});
|
|
152
|
+
|
|
153
|
+
test("calibrating on the hard band beats calibrating on everything", () => {
|
|
154
|
+
// Three anchors published 20/50/80 apart. On the FULL suite they all score ~0.97
|
|
155
|
+
// because easy and moderate pin everyone at the ceiling; on the hard band alone they
|
|
156
|
+
// separate. Same models, same publishing, different x — and only one of them can fit.
|
|
157
|
+
const mk = (slug: string, full: number, hard: number): EvalResult => ({
|
|
158
|
+
slug,
|
|
159
|
+
axes: { coding: { sum: full, n: 1 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } },
|
|
160
|
+
axesHard: { coding: { sum: hard, n: 1 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } },
|
|
161
|
+
errors: 0,
|
|
162
|
+
repeats: 1,
|
|
163
|
+
spread: {},
|
|
164
|
+
byComplexity: {},
|
|
165
|
+
});
|
|
166
|
+
const anchors = [mk("a/low", 0.96, 0.2), mk("a/mid", 0.97, 0.5), mk("a/high", 0.98, 0.8)];
|
|
167
|
+
const aa: Record<string, number> = { "a/low": 20, "a/mid": 50, "a/high": 80 };
|
|
168
|
+
const published = (slug: string, axis: QualityAxis) => (axis === "coding" ? aa[slug] : undefined);
|
|
169
|
+
const target = mk("z/target", 0.97, 0.5);
|
|
170
|
+
|
|
171
|
+
const onHard = toLocalFeedScores([target], fitCalibration(anchors, published, hardRaw), () => "z", hardRaw);
|
|
172
|
+
// The hard band spans 0.2-0.8 against 20-80, so the fit is a real line: raw 0.5 ⇒ ~50.
|
|
173
|
+
expect(onHard[0]!.coding).toBeCloseTo(50, 0);
|
|
174
|
+
|
|
175
|
+
// On the full suite the anchors span 0.96-0.98: the same published spread compressed into
|
|
176
|
+
// a fiftieth of the range. That is the shallow slope that produced 22.8 for a model
|
|
177
|
+
// published at 39.5, so the fit must be refused rather than published.
|
|
178
|
+
const pooledFit = fitCalibration(anchors, published);
|
|
179
|
+
const pooled = toLocalFeedScores([target], pooledFit, () => "z");
|
|
180
|
+
expect(pooled).toEqual([]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("a weak fit is refused, not published", () => {
|
|
184
|
+
// Points with a real but noisy relationship: computable (r >= MIN_R) yet not worth
|
|
185
|
+
// acting on. `r` and `n` used to be computed and then thrown away.
|
|
186
|
+
const noisy = [
|
|
187
|
+
{ raw: 0.1, aa: 20 },
|
|
188
|
+
{ raw: 0.5, aa: 70 },
|
|
189
|
+
{ raw: 0.6, aa: 30 },
|
|
190
|
+
{ raw: 0.9, aa: 60 },
|
|
191
|
+
];
|
|
192
|
+
const fit = fitAxis(noisy)!;
|
|
193
|
+
expect(fit.r).toBeGreaterThanOrEqual(MIN_R);
|
|
194
|
+
expect(fit.r).toBeLessThan(PUBLISH_MIN_R);
|
|
195
|
+
const target: EvalResult = {
|
|
196
|
+
slug: "z/t",
|
|
197
|
+
axes: { coding: { sum: 0.5, n: 1 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } },
|
|
198
|
+
axesHard: { coding: { sum: 0.5, n: 1 }, intelligence: { sum: 0, n: 0 }, agentic: { sum: 0, n: 0 } },
|
|
199
|
+
errors: 0,
|
|
200
|
+
repeats: 1,
|
|
201
|
+
spread: {},
|
|
202
|
+
byComplexity: {},
|
|
203
|
+
};
|
|
204
|
+
expect(toLocalFeedScores([target], { coding: fit }, () => "z", hardRaw)).toEqual([]);
|
|
205
|
+
// A caller that deliberately lowers the bar still can, so the gate is policy not dogma.
|
|
206
|
+
expect(toLocalFeedScores([target], { coding: fit }, () => "z", hardRaw, MIN_R)[0]!.coding).toBeGreaterThan(0);
|
|
207
|
+
});
|
|
151
208
|
});
|
|
152
209
|
|
|
153
210
|
describe("runEval", () => {
|