catherd-cli 1.1.1 → 1.2.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/README.md +40 -14
- package/catalog/ATTRIBUTION.md +36 -0
- package/catalog/models.json +30 -0
- package/catalog/scores.json +3006 -194
- package/catalog/sources.json +87 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/skills/catherd/SKILL.md +2 -2
- package/src/adapters/claude-code/models.ts +2 -1
- package/src/domain/bars.ts +119 -0
- package/src/domain/calibration.ts +125 -0
- package/src/domain/catalog.ts +210 -23
- package/src/domain/failover.ts +14 -1
- package/src/domain/profile-rules.ts +108 -18
- package/src/domain/sources.ts +144 -0
- package/src/entry/catalog-command.ts +163 -8
- package/src/entry/init-command.ts +72 -2
- package/src/entry/mcp/lane-tools.ts +1 -1
- package/src/entry/mcp/server.ts +13 -3
- package/src/entry/mcp/setup-tools.ts +24 -2
- package/src/entry/profile-command.ts +10 -4
- package/src/entry/tui/commands.ts +20 -2
- package/src/entry/tui/effects.ts +50 -1
- package/src/entry/tui/fixtures.ts +31 -3
- package/src/entry/tui/profile-edits.ts +17 -9
- package/src/entry/tui/profile-tree.ts +2 -2
- package/src/entry/tui/state.ts +4 -0
- package/src/entry/tui/views/profile-actions.ts +9 -1
- package/src/entry/tui/views/profiles.tsx +81 -14
- package/src/entry/tui/views/save-dialog.tsx +12 -4
- package/src/infra/env.ts +2 -0
- package/src/infra/jev-client.ts +88 -40
- package/src/infra/paths.ts +2 -0
- package/src/infra/sources/arena.ts +87 -0
- package/src/infra/sources/artificial-analysis.ts +112 -0
- package/src/infra/sources/cache.ts +124 -0
- package/src/infra/sources/csv.ts +38 -0
- package/src/infra/sources/epoch.ts +82 -0
- package/src/infra/sources/http.ts +75 -0
- package/src/infra/sources/litellm.ts +58 -0
- package/src/infra/sources/models-dev.ts +59 -0
- package/src/infra/sources/openrouter-endpoints.ts +52 -0
- package/src/infra/sources/openrouter-models.ts +60 -0
- package/src/infra/sources/rows.ts +38 -0
- package/src/infra/sources/vectara.ts +69 -0
- package/src/infra/sources/zip.ts +48 -0
- package/src/services/catalog-refresh.ts +200 -0
- package/src/services/catalog-service.ts +83 -23
- package/src/services/credentials.ts +69 -0
- package/src/services/doctor-sources.ts +76 -0
- package/src/services/doctor.ts +10 -0
- package/src/services/jev-service.ts +14 -41
- package/src/services/ports.ts +6 -0
- package/src/services/profile-service.ts +8 -6
- package/src/services/provenance.ts +115 -0
- package/src/services/routing-service.ts +7 -2
- package/src/services/run-evidence.ts +86 -0
- package/src/services/source-derive.ts +349 -0
- package/src/services/source-sync.ts +305 -0
- package/src/services/standins.ts +204 -0
- package/src/services/treat-likes.ts +84 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"version": "2026-09-28",
|
|
4
|
+
"sources": [
|
|
5
|
+
{
|
|
6
|
+
"id": "models-dev",
|
|
7
|
+
"name": "models.dev",
|
|
8
|
+
"url": "https://models.dev/api.json",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"attribution": "Model facts from models.dev (MIT License), https://models.dev",
|
|
11
|
+
"keyed": false
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "openrouter-models",
|
|
15
|
+
"name": "OpenRouter models",
|
|
16
|
+
"url": "https://openrouter.ai/api/v1/models",
|
|
17
|
+
"license": "public API",
|
|
18
|
+
"attribution": "Model listings from the OpenRouter API, https://openrouter.ai",
|
|
19
|
+
"keyed": false
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "openrouter-endpoints",
|
|
23
|
+
"name": "OpenRouter endpoints",
|
|
24
|
+
"url": "https://openrouter.ai/api/v1/models",
|
|
25
|
+
"license": "public API",
|
|
26
|
+
"attribution": "Endpoint latency and throughput from the OpenRouter API, https://openrouter.ai",
|
|
27
|
+
"keyed": false
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "litellm",
|
|
31
|
+
"name": "LiteLLM",
|
|
32
|
+
"url": "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"attribution": "Prices from LiteLLM's model_prices_and_context_window.json (MIT License), https://github.com/BerriAI/litellm",
|
|
35
|
+
"keyed": false
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "arena",
|
|
39
|
+
"name": "Arena (LMArena)",
|
|
40
|
+
"url": "https://huggingface.co/datasets/lmarena-ai/leaderboard-dataset",
|
|
41
|
+
"license": "CC-BY-4.0",
|
|
42
|
+
"attribution": "Leaderboard data by LMArena, CC BY 4.0, https://huggingface.co/datasets/lmarena-ai/leaderboard-dataset",
|
|
43
|
+
"keyed": false
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "vectara",
|
|
47
|
+
"name": "Vectara hallucination leaderboard",
|
|
48
|
+
"url": "https://github.com/vectara/hallucination-leaderboard",
|
|
49
|
+
"license": "Apache-2.0",
|
|
50
|
+
"attribution": "Hallucination Leaderboard by Vectara (Apache License 2.0), https://github.com/vectara/hallucination-leaderboard",
|
|
51
|
+
"keyed": false
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "epoch",
|
|
55
|
+
"name": "Epoch AI benchmarks",
|
|
56
|
+
"url": "https://epoch.ai/data/benchmark_data.zip",
|
|
57
|
+
"license": "CC-BY-4.0; external tables keep their own license",
|
|
58
|
+
"attribution": "Epoch AI, 'Capabilities & benchmarking'. Published online at epoch.ai. Retrieved from 'https://epoch.ai/benchmarks' (CC BY 4.0)",
|
|
59
|
+
"keyed": false
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "artificial-analysis",
|
|
63
|
+
"name": "Artificial Analysis",
|
|
64
|
+
"url": "https://artificialanalysis.ai/api/v2/data/llms/models",
|
|
65
|
+
"license": "free key, internal use, attribution; read with the user's own key and never shipped",
|
|
66
|
+
"attribution": "Data from Artificial Analysis, https://artificialanalysis.ai",
|
|
67
|
+
"keyed": true
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"aliases": {
|
|
71
|
+
"claude-4-5-haiku": "claude-haiku-4-5",
|
|
72
|
+
"claude-haiku-4-5-20251001": "claude-haiku-4-5"
|
|
73
|
+
},
|
|
74
|
+
"defaultEffort": {
|
|
75
|
+
"gpt-6-astra": "medium",
|
|
76
|
+
"gpt-6-sol": "medium",
|
|
77
|
+
"gpt-6-luna": "medium",
|
|
78
|
+
"gpt-5.6-sol": "medium",
|
|
79
|
+
"gpt-5.6-terra": "medium",
|
|
80
|
+
"gpt-5.6-luna": "medium",
|
|
81
|
+
"claude-fable-5-1": "high",
|
|
82
|
+
"claude-opus-5-5": "high",
|
|
83
|
+
"claude-sonnet-5-5": "high",
|
|
84
|
+
"claude-sonnet-5": "high",
|
|
85
|
+
"claude-haiku-4-5": "default"
|
|
86
|
+
}
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "catherd",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Herds coding agents: Claude plans and verifies, Codex, opencode or headless Claude Code workers write the code, and Jev picks the model and effort for each piece.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "47vigen",
|
package/plugin/bin/catherd-mcp
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# catherd's MCP launcher (spec 1.1 §12): the plugin's .mcp.json runs it. The release stamps VERSION
|
|
3
3
|
# (scripts/stamp-plugin-version.mjs), so the plugin always starts the server it was released with.
|
|
4
|
-
VERSION="1.
|
|
4
|
+
VERSION="1.2.1"
|
|
5
5
|
|
|
6
6
|
# a global catherd at this version (catherd init installs one): start it, no resolve at all
|
|
7
7
|
if command -v catherd >/dev/null 2>&1 && [ "$(catherd --version 2>/dev/null)" = "$VERSION" ]; then
|
|
@@ -23,7 +23,7 @@ Your job:
|
|
|
23
23
|
|
|
24
24
|
The catherd server keeps `state.md` true: it rewrites it on every dispatch, climb and landing.
|
|
25
25
|
|
|
26
|
-
**Your first call is `status()`.** Its `version` must be the one this plugin pins, `catherd-cli@1.
|
|
26
|
+
**Your first call is `status()`.** Its `version` must be the one this plugin pins, `catherd-cli@1.2.1`. If it differs, stop and tell the user to restart Claude Code so the plugin and its server match.
|
|
27
27
|
|
|
28
28
|
**You never edit product files.** Every line of code, tests or docs comes from a role, including a one-line fix.
|
|
29
29
|
|
|
@@ -274,7 +274,7 @@ The brief is the `brief` text you pass to `dispatch` (catherd writes it to the d
|
|
|
274
274
|
4. For a worker:
|
|
275
275
|
- "Read `<R>/lanes/Mx.Ly.md`": decisions, signatures, data shapes;
|
|
276
276
|
- its fast check (targeted tests, lint and type check), to run until it passes;
|
|
277
|
-
- "Run the full suite only if this brief says so", and "Wrap any full build or full test suite in `bunx catherd-cli@1.
|
|
277
|
+
- "Run the full suite only if this brief says so", and "Wrap any full build or full test suite in `bunx catherd-cli@1.2.1 lock -- <command>`": other lanes share the machine.
|
|
278
278
|
5. For a reviewer: the A-lines and the changed files, with new files read in full. It reports every finding as `BLOCKER|BUG|NIT file:line — problem — fix`, covering:
|
|
279
279
|
- unmet A-lines and edge cases;
|
|
280
280
|
- code or abstractions nobody needs;
|
|
@@ -9,6 +9,7 @@ export const CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
|
9
9
|
export const CLAUDE_MODELS: DiscoveredModel[] = [
|
|
10
10
|
{ id: "claude-fable-5-1", efforts: CLAUDE_EFFORTS, context: 1_000_000, imageIn: true },
|
|
11
11
|
{ id: "claude-opus-5-5", efforts: CLAUDE_EFFORTS, context: 1_000_000, imageIn: true },
|
|
12
|
+
{ id: "claude-sonnet-5-5", efforts: CLAUDE_EFFORTS, context: 1_000_000, imageIn: true },
|
|
12
13
|
{ id: "claude-sonnet-5", efforts: CLAUDE_EFFORTS, context: 1_000_000, imageIn: true },
|
|
13
14
|
{ id: "claude-haiku-4-5-20251001", efforts: [], context: 200_000, imageIn: true },
|
|
14
15
|
];
|
|
@@ -20,7 +21,7 @@ export const CLAUDE_ALIASES: Record<string, string> = {
|
|
|
20
21
|
opus: "claude-opus-5-5",
|
|
21
22
|
opusplan: "claude-opus-5-5",
|
|
22
23
|
default: "claude-opus-5-5",
|
|
23
|
-
sonnet: "claude-sonnet-5",
|
|
24
|
+
sonnet: "claude-sonnet-5-5",
|
|
24
25
|
haiku: "claude-haiku-4-5-20251001",
|
|
25
26
|
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
|
|
26
27
|
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type Dim, DIMS, type Score } from "./catalog.ts";
|
|
2
|
+
import { DIFFICULTIES, type Difficulty, KINDS, type Kind } from "./lane.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spec 1.2 §5.1: the dimensions each kind's default bars span, for Track A (`copy`, `build`) and Track B
|
|
6
|
+
* (`logic`, `hard`). `steer` has no default bar: it is shown, and a user may set one.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_BAR_DIMS: Record<Kind, { A: Dim[]; B: Dim[] }> = {
|
|
9
|
+
repo_code: { A: ["repo_code"], B: ["repo_code", "honesty", "agentic"] },
|
|
10
|
+
terminal: { A: ["terminal"], B: ["terminal", "honesty", "agentic"] },
|
|
11
|
+
ui: { A: ["frontend", "repo_code"], B: ["frontend", "repo_code", "honesty", "agentic"] },
|
|
12
|
+
prose: { A: ["repo_code"], B: ["repo_code", "honesty"] },
|
|
13
|
+
research: { A: ["repo_code"], B: ["repo_code", "honesty"] },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** The dimensions a difficulty's default bar spans for a kind. */
|
|
17
|
+
export const barDimsOf = (kind: Kind, d: Difficulty): Dim[] =>
|
|
18
|
+
d === "copy" || d === "build" ? DEFAULT_BAR_DIMS[kind].A : DEFAULT_BAR_DIMS[kind].B;
|
|
19
|
+
|
|
20
|
+
/** Every dimension some default bar uses: the values an enabled rung needs (spec 1.2 §6.1). */
|
|
21
|
+
export const BAR_DIMS: Dim[] = DIMS.filter((d) =>
|
|
22
|
+
KINDS.some((k) => DIFFICULTIES.some((x) => barDimsOf(k, x).includes(d))),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/** Spec 1.2 §5.2: the percentile of the scored rungs each difficulty's threshold sits at. */
|
|
26
|
+
export const BAR_PERCENTILE: Record<Difficulty, number> = { copy: 25, build: 50, logic: 60, hard: 75 };
|
|
27
|
+
|
|
28
|
+
const PERCENTILE_WORD: Record<Difficulty, string> = {
|
|
29
|
+
copy: "the 25th percentile",
|
|
30
|
+
build: "the median",
|
|
31
|
+
logic: "the 60th percentile",
|
|
32
|
+
hard: "the 75th percentile",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The `p`th percentile of `xs`, interpolated linearly between the closest ranks; NaN for none. */
|
|
36
|
+
export function percentile(xs: readonly number[], p: number): number {
|
|
37
|
+
if (xs.length === 0) return Number.NaN;
|
|
38
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
39
|
+
const pos = ((s.length - 1) * p) / 100;
|
|
40
|
+
const lo = Math.floor(pos);
|
|
41
|
+
const hi = Math.ceil(pos);
|
|
42
|
+
return (s[lo] as number) + ((s[hi] as number) - (s[lo] as number)) * (pos - lo);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Spec 1.2 §5.2 "measured or better": a value published for this exact rung in the anchor's unit. That is a
|
|
47
|
+
* `verified` or `measured` value, and a hand-typed `secondary` one (no `source`: the 1.0 word for a value
|
|
48
|
+
* published by someone other than the vendor, which is 1.2's `measured`). Plan 14 Ruling 1.
|
|
49
|
+
*/
|
|
50
|
+
export const measuredOrBetter = (s: Score): boolean =>
|
|
51
|
+
s.confidence === "verified" ||
|
|
52
|
+
s.confidence === "measured" ||
|
|
53
|
+
(s.confidence === "secondary" && s.source === undefined);
|
|
54
|
+
|
|
55
|
+
/** Each dimension's values that bars are derived from: one per canonical rung, measured or better. */
|
|
56
|
+
export function barPool(scores: readonly Score[]): Record<Dim, number[]> {
|
|
57
|
+
const out = Object.fromEntries(DIMS.map((d) => [d, [] as number[]])) as Record<Dim, number[]>;
|
|
58
|
+
const seen = new Set<string>();
|
|
59
|
+
for (const s of scores) {
|
|
60
|
+
const key = `${s.rung} ${s.dim}`;
|
|
61
|
+
if (!measuredOrBetter(s) || seen.has(key)) continue;
|
|
62
|
+
seen.add(key);
|
|
63
|
+
out[s.dim].push(s.value);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A bar value, kept to four significant digits (DeepSWE 60.95, an Arena rating 1668, agentic 0.08622). */
|
|
69
|
+
const tidy = (v: number): number => Number(v.toPrecision(4));
|
|
70
|
+
|
|
71
|
+
export type Bars = Record<Kind, Record<Difficulty, Partial<Record<Dim, number>>>>;
|
|
72
|
+
/** `scores.json` `barsWhy`: per dimension and difficulty, where its threshold came from. */
|
|
73
|
+
export type BarsWhy = Partial<Record<Dim, Partial<Record<Difficulty, string>>>>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Spec 1.2 §5.1–§5.2: the default bars. Each dimension's threshold at a difficulty is that difficulty's
|
|
77
|
+
* percentile of the pool (`barPool`), the same in every kind whose bar spans the dimension. A dimension with
|
|
78
|
+
* no pooled value gets no threshold (and a `barsWhy` line saying so). `benchmark` names each dimension's unit
|
|
79
|
+
* and `date` the day the data was read.
|
|
80
|
+
*/
|
|
81
|
+
export function deriveBars(
|
|
82
|
+
pool: Record<Dim, number[]>,
|
|
83
|
+
benchmark: (d: Dim) => string,
|
|
84
|
+
date: string,
|
|
85
|
+
): { bars: Bars; barsWhy: BarsWhy } {
|
|
86
|
+
const value: Partial<Record<Dim, Partial<Record<Difficulty, number>>>> = {};
|
|
87
|
+
const barsWhy: BarsWhy = {};
|
|
88
|
+
for (const dim of BAR_DIMS)
|
|
89
|
+
for (const d of DIFFICULTIES) {
|
|
90
|
+
if (!KINDS.some((k) => barDimsOf(k, d).includes(dim))) continue;
|
|
91
|
+
const xs = pool[dim];
|
|
92
|
+
const why = (barsWhy[dim] ??= {});
|
|
93
|
+
if (xs.length === 0) {
|
|
94
|
+
why[d] = `no rung is measured or better on ${benchmark(dim)} (${date}): no threshold`;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const v = tidy(percentile(xs, BAR_PERCENTILE[d]));
|
|
98
|
+
(value[dim] ??= {})[d] = v;
|
|
99
|
+
why[d] =
|
|
100
|
+
`${PERCENTILE_WORD[d]} of ${xs.length} rungs measured or better on ${benchmark(dim)} (${date}): ${v}`;
|
|
101
|
+
}
|
|
102
|
+
const bars = Object.fromEntries(
|
|
103
|
+
KINDS.map((k) => [
|
|
104
|
+
k,
|
|
105
|
+
Object.fromEntries(
|
|
106
|
+
DIFFICULTIES.map((d) => [
|
|
107
|
+
d,
|
|
108
|
+
Object.fromEntries(
|
|
109
|
+
barDimsOf(k, d).flatMap((dim) => {
|
|
110
|
+
const v = value[dim]?.[d];
|
|
111
|
+
return v === undefined ? [] : [[dim, v]];
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
114
|
+
]),
|
|
115
|
+
),
|
|
116
|
+
]),
|
|
117
|
+
) as Bars;
|
|
118
|
+
return { bars, barsWhy };
|
|
119
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Dim } from "./catalog.ts";
|
|
2
|
+
import type { SourceId } from "./sources.ts";
|
|
3
|
+
|
|
4
|
+
/** Spec 1.2 §4.2: a source needs this many rungs shared with the anchor for a dimension. */
|
|
5
|
+
export const MIN_SHARED = 5;
|
|
6
|
+
/** Spec 1.2 §4.2: a fit below this R² is not used. */
|
|
7
|
+
export const MIN_R2 = 0.5;
|
|
8
|
+
|
|
9
|
+
/** One number a source parser returns: `field` of `source`, and the benchmark it names. */
|
|
10
|
+
export interface FieldRef {
|
|
11
|
+
source: SourceId;
|
|
12
|
+
field: string;
|
|
13
|
+
benchmark: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Spec 1.2 §4.1: each dimension's anchor, the unit its bars are in, and the sources calibrated onto it.
|
|
18
|
+
* `shipped` is the hand-typed value `catalog/scores.json` carries, published for that rung (every anchor is
|
|
19
|
+
* keyless, spec 1.2 §4.1).
|
|
20
|
+
*/
|
|
21
|
+
export const DIM_SOURCES: Record<Dim, { anchor: FieldRef | "shipped"; others: FieldRef[] }> = {
|
|
22
|
+
repo_code: {
|
|
23
|
+
anchor: "shipped",
|
|
24
|
+
others: [
|
|
25
|
+
{
|
|
26
|
+
source: "artificial-analysis",
|
|
27
|
+
field: "livecodebench",
|
|
28
|
+
benchmark: "LiveCodeBench (Artificial Analysis)",
|
|
29
|
+
},
|
|
30
|
+
{ source: "artificial-analysis", field: "scicode", benchmark: "SciCode (Artificial Analysis)" },
|
|
31
|
+
{
|
|
32
|
+
source: "artificial-analysis",
|
|
33
|
+
field: "artificial_analysis_coding_index",
|
|
34
|
+
benchmark: "Artificial Analysis Coding Index",
|
|
35
|
+
},
|
|
36
|
+
{ source: "epoch", field: "frontiercode", benchmark: "FrontierCode (Epoch AI)" },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
// plan 14 Ruling C-2: the vendors' Terminal-Bench 4.0 values, not Epoch's Terminal-Bench 2.0, which shares
|
|
40
|
+
// no rung with them; Epoch is fitted onto them once it covers five of their rungs
|
|
41
|
+
terminal: {
|
|
42
|
+
anchor: "shipped",
|
|
43
|
+
others: [
|
|
44
|
+
{ source: "epoch", field: "terminalbench", benchmark: "Terminal-Bench (Epoch AI)" },
|
|
45
|
+
{
|
|
46
|
+
source: "artificial-analysis",
|
|
47
|
+
field: "terminalbench_v2_1",
|
|
48
|
+
benchmark: "Terminal-Bench 2.1 (Artificial Analysis)",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
honesty: {
|
|
53
|
+
anchor: "shipped",
|
|
54
|
+
others: [
|
|
55
|
+
{ source: "vectara", field: "factual_consistency", benchmark: "Vectara factual consistency rate" },
|
|
56
|
+
{ source: "arena", field: "agent_tool_hallucination", benchmark: "Arena agent tool hallucination" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
agentic: {
|
|
60
|
+
anchor: { source: "arena", field: "agent", benchmark: "Arena agent, net improvement" },
|
|
61
|
+
others: [
|
|
62
|
+
{ source: "arena", field: "agent_task_outcome_explicit", benchmark: "Arena agent task outcome" },
|
|
63
|
+
{ source: "arena", field: "agent_bash_recovery_steps", benchmark: "Arena agent bash recovery" },
|
|
64
|
+
{ source: "artificial-analysis", field: "tau2", benchmark: "tau2-bench (Artificial Analysis)" },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
steer: {
|
|
68
|
+
anchor: { source: "arena", field: "agent_steerability", benchmark: "Arena agent steerability" },
|
|
69
|
+
others: [],
|
|
70
|
+
},
|
|
71
|
+
frontend: {
|
|
72
|
+
anchor: { source: "arena", field: "webdev", benchmark: "Arena WebDev rating" },
|
|
73
|
+
others: [{ source: "epoch", field: "webdev", benchmark: "WebDev Arena (Epoch AI)" }],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export interface LineFit {
|
|
78
|
+
a: number;
|
|
79
|
+
b: number;
|
|
80
|
+
r2: number;
|
|
81
|
+
n: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Least squares `anchor = a·x + b` over `[x, anchor]` pairs; null with fewer than 2 pairs or no spread in x. */
|
|
85
|
+
export function fitLine(pairs: readonly (readonly [number, number])[]): LineFit | null {
|
|
86
|
+
const n = pairs.length;
|
|
87
|
+
if (n < 2) return null;
|
|
88
|
+
const mx = pairs.reduce((s, [x]) => s + x, 0) / n;
|
|
89
|
+
const my = pairs.reduce((s, [, y]) => s + y, 0) / n;
|
|
90
|
+
let sxx = 0;
|
|
91
|
+
let sxy = 0;
|
|
92
|
+
let syy = 0;
|
|
93
|
+
for (const [x, y] of pairs) {
|
|
94
|
+
sxx += (x - mx) ** 2;
|
|
95
|
+
sxy += (x - mx) * (y - my);
|
|
96
|
+
syy += (y - my) ** 2;
|
|
97
|
+
}
|
|
98
|
+
if (sxx === 0) return null;
|
|
99
|
+
const a = sxy / sxx;
|
|
100
|
+
const b = my - a * mx;
|
|
101
|
+
const res = pairs.reduce((s, [x, y]) => s + (y - (a * x + b)) ** 2, 0);
|
|
102
|
+
return { a, b, r2: syy === 0 ? 0 : 1 - res / syy, n };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Spec 1.2 §4.2: the fit of `other` onto `anchor` over the keys both hold, and whether it may be used: at
|
|
107
|
+
* least MIN_SHARED shared rungs and an R² of at least MIN_R2. `why` says why not.
|
|
108
|
+
*/
|
|
109
|
+
export function calibrate(
|
|
110
|
+
anchor: ReadonlyMap<string, number>,
|
|
111
|
+
other: ReadonlyMap<string, number>,
|
|
112
|
+
): { fit: LineFit | null; n: number; used: boolean; why?: string } {
|
|
113
|
+
const pairs: [number, number][] = [];
|
|
114
|
+
for (const [k, x] of other) {
|
|
115
|
+
const y = anchor.get(k);
|
|
116
|
+
if (y !== undefined) pairs.push([x, y]);
|
|
117
|
+
}
|
|
118
|
+
const n = pairs.length;
|
|
119
|
+
if (n < MIN_SHARED)
|
|
120
|
+
return { fit: null, n, used: false, why: `${n} shared rungs; a fit needs ${MIN_SHARED}` };
|
|
121
|
+
const fit = fitLine(pairs);
|
|
122
|
+
if (!fit) return { fit: null, n, used: false, why: "the source gives every shared rung the same value" };
|
|
123
|
+
if (fit.r2 < MIN_R2) return { fit, n, used: false, why: `R² ${fit.r2.toFixed(2)} is below ${MIN_R2}` };
|
|
124
|
+
return { fit, n, used: true };
|
|
125
|
+
}
|