catherd-cli 1.5.0 → 2.0.0-beta.20261022113309
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 +164 -208
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -247
- package/package.json +6 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/commands/catherd.md +2 -2
- package/plugin/skills/catherd/SKILL.md +97 -421
- package/plugin/skills/catherd-setup/SKILL.md +20 -19
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +1 -2
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +4 -4
- package/src/cli.ts +22 -27
- package/src/domain/bars.ts +33 -107
- package/src/domain/blocked.ts +72 -0
- package/src/domain/branch.ts +51 -0
- package/src/domain/brief.ts +21 -6
- package/src/domain/catalog.ts +69 -324
- package/src/domain/cost.ts +15 -30
- package/src/domain/errors.ts +2 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -87
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +1 -0
- package/src/domain/ids.ts +13 -2
- package/src/domain/jev.ts +47 -2
- package/src/domain/judge.ts +124 -0
- package/src/domain/lane.ts +22 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +25 -8
- package/src/domain/notify.ts +32 -0
- package/src/domain/plan-doc.ts +198 -0
- package/src/domain/plan-lint.ts +141 -0
- package/src/domain/profile-rules.ts +101 -160
- package/src/domain/profile.ts +116 -63
- package/src/domain/record.ts +53 -2
- package/src/domain/role-prompts.ts +131 -45
- package/src/domain/role-scope.ts +4 -34
- package/src/domain/role-tools.ts +2 -2
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +17 -0
- package/src/domain/rules.ts +33 -0
- package/src/domain/run-events.ts +298 -0
- package/src/domain/run-machine.ts +668 -0
- package/src/domain/run-state.ts +1310 -0
- package/src/domain/scorecard.ts +197 -0
- package/src/domain/select.ts +151 -146
- package/src/domain/slopalytics.ts +72 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +25 -174
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +1 -1
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/init-command.ts +18 -68
- package/src/entry/knowledge-command.ts +45 -4
- package/src/entry/lock-command.ts +6 -7
- package/src/entry/mcp/role-server.ts +18 -0
- package/src/entry/mcp/server.ts +13 -40
- package/src/entry/mcp/session-tools.ts +149 -0
- package/src/entry/panel/openapi.ts +189 -0
- package/src/entry/panel/server.ts +410 -0
- package/src/entry/panel-command.ts +68 -0
- package/src/entry/profile-command.ts +10 -14
- package/src/entry/role-cli-command.ts +47 -3
- package/src/entry/runs-command.ts +3 -19
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/infra/env.ts +0 -2
- package/src/infra/git.ts +64 -0
- package/src/infra/heavy-lock.ts +45 -7
- package/src/infra/launch.ts +63 -7
- package/src/infra/memory.ts +45 -0
- package/src/infra/ntfy.ts +40 -0
- package/src/infra/open-url.ts +42 -0
- package/src/infra/paths.ts +2 -2
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/services/admission.ts +24 -9
- package/src/services/backends.ts +4 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -262
- package/src/services/credentials.ts +4 -20
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +5 -1
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor.ts +4 -9
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +12 -0
- package/src/services/gate-env.ts +1 -2
- package/src/services/gate-service.ts +21 -6
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-service.ts +34 -3
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +35 -8
- package/src/services/notifier.ts +69 -2
- package/src/services/notify-service.ts +26 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +30 -4
- package/src/services/preflight.ts +82 -8
- package/src/services/profile-service.ts +30 -3
- package/src/services/profile-store.ts +8 -3
- package/src/services/protocol.ts +4 -0
- package/src/services/provenance.ts +31 -90
- package/src/services/questions.ts +9 -0
- package/src/services/reconcile.ts +11 -0
- package/src/services/routing-service.ts +26 -15
- package/src/services/run-briefs.ts +477 -0
- package/src/services/run-client.ts +217 -0
- package/src/services/run-effects.ts +837 -0
- package/src/services/run-events-store.ts +174 -0
- package/src/services/run-evidence.ts +1 -1
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +55 -6
- package/src/services/run-store.ts +54 -7
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/summary.ts +27 -3
- package/src/services/tree-path.ts +27 -0
- package/src/services/workspace-admission.ts +5 -13
- package/src/services/workspace-service.ts +5 -0
- package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js +1 -0
- package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
- package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js +1 -0
- package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-B6GiwDkh.js +11 -0
- package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
- package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
- package/web/dist/assets/index-C5IbylvM.css +2 -0
- package/web/dist/assets/index-C5IbylvM.css.br +0 -0
- package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
- package/web/dist/assets/label-DFSYBc62.js +1 -0
- package/web/dist/assets/label-DFSYBc62.js.br +0 -0
- package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
- package/web/dist/assets/link-D8EcKZUt.js +1 -0
- package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
- package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
- package/web/dist/assets/login-DaN5HPPW.js +1 -0
- package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
- package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
- package/web/dist/assets/query-Cu--ccT3.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
- package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
- package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
- package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
- package/web/dist/assets/table-BOxXhqS4.js +1 -0
- package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
- package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
- package/web/dist/assets/time--9kSQbbp.js +1 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
- package/web/dist/index.html +15 -0
- package/catalog/ATTRIBUTION.md +0 -44
- package/catalog/scores.json +0 -3202
- package/catalog/sources.json +0 -97
- package/src/adapters/antigravity/events.ts +0 -131
- package/src/adapters/antigravity/home.ts +0 -46
- package/src/adapters/antigravity/index.ts +0 -335
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -187
- package/src/adapters/grok/index.ts +0 -358
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -132
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -131
- package/src/entry/mcp/protocol-tools.ts +0 -63
- package/src/entry/mcp/push-tools.ts +0 -16
- package/src/entry/mcp/run-tools.ts +0 -164
- package/src/entry/mcp/setup-tools.ts +0 -109
- package/src/entry/mcp/workspace-tools.ts +0 -142
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -385
- package/src/entry/tui/fixtures.ts +0 -637
- package/src/entry/tui/profile-edits.ts +0 -146
- package/src/entry/tui/profile-tree.ts +0 -429
- package/src/entry/tui/providers/app.tsx +0 -162
- package/src/entry/tui/providers/boundary.tsx +0 -57
- package/src/entry/tui/providers/data.tsx +0 -160
- package/src/entry/tui/providers/keymap.tsx +0 -166
- package/src/entry/tui/providers/root.tsx +0 -45
- package/src/entry/tui/providers/theme.tsx +0 -17
- package/src/entry/tui/providers/toast.tsx +0 -77
- package/src/entry/tui/run.tsx +0 -114
- package/src/entry/tui/state.ts +0 -400
- package/src/entry/tui/stories.ts +0 -114
- package/src/entry/tui/text.ts +0 -130
- package/src/entry/tui/theme.ts +0 -140
- package/src/entry/tui/views/app.tsx +0 -208
- package/src/entry/tui/views/dialogs.tsx +0 -41
- package/src/entry/tui/views/profile-actions.ts +0 -331
- package/src/entry/tui/views/profiles.tsx +0 -520
- package/src/entry/tui/views/runs.tsx +0 -619
- package/src/entry/tui/views/save-dialog.tsx +0 -344
- package/src/entry/tui/views/status.tsx +0 -213
- package/src/entry/tui/widgets/chrome.tsx +0 -114
- package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
- package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
- package/src/entry/tui/widgets/dialog-select.tsx +0 -212
- package/src/entry/tui/widgets/dialog.tsx +0 -68
- package/src/entry/tui/widgets/line.tsx +0 -92
- package/src/entry/tui/widgets/list.tsx +0 -197
- package/src/entry/tui/widgets/spinner.tsx +0 -49
- package/src/entry/tui/widgets/toast.tsx +0 -62
- package/src/infra/sources/arena.ts +0 -95
- package/src/infra/sources/artificial-analysis.ts +0 -119
- package/src/infra/sources/cache.ts +0 -126
- package/src/infra/sources/csv.ts +0 -38
- package/src/infra/sources/epoch.ts +0 -82
- package/src/infra/sources/http.ts +0 -75
- package/src/infra/sources/litellm.ts +0 -58
- package/src/infra/sources/models-dev.ts +0 -59
- package/src/infra/sources/openrouter-endpoints.ts +0 -52
- package/src/infra/sources/openrouter-models.ts +0 -60
- package/src/infra/sources/rows.ts +0 -38
- package/src/infra/sources/vectara.ts +0 -69
- package/src/infra/sources/zip.ts +0 -48
- package/src/services/catalog-refresh.ts +0 -201
- package/src/services/doctor-sources.ts +0 -76
- package/src/services/runs-page.ts +0 -290
- package/src/services/source-derive.ts +0 -397
- package/src/services/source-sync.ts +0 -312
- package/src/services/standins.ts +0 -238
- package/src/services/treat-likes.ts +0 -113
package/src/domain/brief.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { replyContract } from "./role-prompts.ts";
|
|
1
|
+
import { replyContract, roleSections } from "./role-prompts.ts";
|
|
2
2
|
import { ROLE_MCP_SERVER, roleMcpTools } from "./role-tools.ts";
|
|
3
3
|
import type { Role } from "./roles.ts";
|
|
4
4
|
|
|
@@ -13,6 +13,10 @@ export interface BriefContext {
|
|
|
13
13
|
scratch: string | null;
|
|
14
14
|
/** whether the role's harness lists the catherd_role tools */
|
|
15
15
|
roleServer: boolean;
|
|
16
|
+
/** the catherd version whose `lock` a worker's text names */
|
|
17
|
+
version: string;
|
|
18
|
+
/** the plan's preamble, which applies to every task (plan 32); null when the run has none for this role */
|
|
19
|
+
context: string | null;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/** The CLI form of each role server tool, which any role can run from its shell. */
|
|
@@ -28,6 +32,8 @@ function cliForm(tool: string, run: string): string {
|
|
|
28
32
|
return `catherd gate check ${run} --milestone <M> --item <item> --command <command> --paths <path,…>`;
|
|
29
33
|
case "gate_pass":
|
|
30
34
|
return `catherd gate pass ${run} --item <item> --command <command> --paths <path,…> --evidence <evidence>`;
|
|
35
|
+
case "gate_run":
|
|
36
|
+
return `catherd gate run ${run} --milestone <M> --command <command> [--at <base checkout>]`;
|
|
31
37
|
default:
|
|
32
38
|
return tool;
|
|
33
39
|
}
|
|
@@ -66,14 +72,21 @@ export function roleNotes(c: BriefContext): string {
|
|
|
66
72
|
return lines.join("\n");
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
const TASK_OPEN = "<task>\n";
|
|
76
|
+
const TASK_CLOSE = "\n</task>";
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
|
-
* The brief admission writes: the
|
|
71
|
-
* each once. A failover stand-in that reruns an earlier brief gets the lane file as it stands now and
|
|
72
|
-
* never two copies.
|
|
79
|
+
* The brief admission writes: the role's sections around its task (the daemon's text, the lane file and the
|
|
80
|
+
* role's notes), each once. A failover stand-in that reruns an earlier brief gets the lane file as it stands now and
|
|
81
|
+
* its own notes, never two copies.
|
|
73
82
|
*/
|
|
74
83
|
export function composeBrief(brief: string, c: BriefContext): string {
|
|
75
84
|
const contract = replyContract(c.role);
|
|
76
85
|
let body = brief.trimEnd();
|
|
86
|
+
const open = body.indexOf(TASK_OPEN);
|
|
87
|
+
const close = body.lastIndexOf(TASK_CLOSE);
|
|
88
|
+
if (open >= 0 && close > open) body = body.slice(open + TASK_OPEN.length, close).trim();
|
|
89
|
+
// a 1.5-era brief rerun ends with its contract
|
|
77
90
|
if (body.endsWith(contract)) body = body.slice(0, -contract.length).trimEnd();
|
|
78
91
|
if (c.lane) {
|
|
79
92
|
const open = LANE_OPEN(c.lane.id);
|
|
@@ -86,6 +99,8 @@ export function composeBrief(brief: string, c: BriefContext): string {
|
|
|
86
99
|
}
|
|
87
100
|
const notesStart = body.lastIndexOf("\n\ncatherd: you are ");
|
|
88
101
|
if (notesStart >= 0) body = body.slice(0, notesStart).trimEnd();
|
|
89
|
-
const
|
|
90
|
-
|
|
102
|
+
const task = [body, ...(c.lane ? [laneBlock(c.lane.id, c.lane.text)] : []), roleNotes(c)]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join("\n\n");
|
|
105
|
+
return `${roleSections(c.role, c.version, { context: c.context, task })}\n`;
|
|
91
106
|
}
|
package/src/domain/catalog.ts
CHANGED
|
@@ -1,59 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type Rung, parseRung } from "./ids.ts";
|
|
3
|
-
import { DIFFICULTIES, type Difficulty, KINDS, type Kind } from "./lane.ts";
|
|
4
3
|
import type { Role } from "./roles.ts";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Spec §5.2 and 1.2 §5.1: the scored dimensions, each in its anchor's unit. `agentic`, `steer` and `frontend`
|
|
8
|
-
* (1.2) carry no shipped bar yet.
|
|
9
|
-
*/
|
|
10
|
-
export const DIMS = ["repo_code", "terminal", "honesty", "agentic", "steer", "frontend"] as const;
|
|
11
|
-
export type Dim = (typeof DIMS)[number];
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Spec 1.2 §4.3: how a value was obtained, best first. The 1.0 levels (`verified`, `secondary`,
|
|
15
|
-
* `inferred`) keep their names, so an older override file stays valid.
|
|
16
|
-
*/
|
|
17
|
-
export const CONFIDENCE = [
|
|
18
|
-
"verified",
|
|
19
|
-
"measured",
|
|
20
|
-
"calibrated",
|
|
21
|
-
"adjacent",
|
|
22
|
-
"secondary",
|
|
23
|
-
"inferred",
|
|
24
|
-
] as const;
|
|
25
|
-
export type Confidence = (typeof CONFIDENCE)[number];
|
|
26
|
-
export const RANK = Object.fromEntries(CONFIDENCE.map((c, i) => [c, i])) as Record<Confidence, number>;
|
|
27
|
-
/** Spec 1.2 §4.3: a value older than this many days counts one level lower. */
|
|
28
|
-
export const STALE_DAYS = 90;
|
|
29
|
-
const DAY_MS = 86_400_000;
|
|
4
|
+
import { type IndexFile, slopKey } from "./slopalytics.ts";
|
|
30
5
|
|
|
31
6
|
/**
|
|
32
7
|
* Spec §7.1 `billing` keys: a rung's backend, except that opencode's Go models (`opencode-go/…`) are billed
|
|
33
8
|
* apart from its Zen models (`opencode/…`).
|
|
34
9
|
*/
|
|
35
|
-
export const BILLING_KEYS = [
|
|
36
|
-
"codex",
|
|
37
|
-
"claude",
|
|
38
|
-
"claude-code",
|
|
39
|
-
"opencode-go",
|
|
40
|
-
"opencode",
|
|
41
|
-
"cursor",
|
|
42
|
-
"grok",
|
|
43
|
-
"antigravity",
|
|
44
|
-
] as const;
|
|
10
|
+
export const BILLING_KEYS = ["codex", "claude", "claude-code", "opencode-go", "opencode", "cursor"] as const;
|
|
45
11
|
export type BillingKey = (typeof BILLING_KEYS)[number];
|
|
46
12
|
|
|
47
13
|
/** The keys a family's `on` map uses; the native `claude` pseudo-backend runs claude-code's model ids. */
|
|
48
|
-
const MODEL_KEYS = [
|
|
49
|
-
"codex",
|
|
50
|
-
"claude-code",
|
|
51
|
-
"opencode-go",
|
|
52
|
-
"opencode",
|
|
53
|
-
"cursor",
|
|
54
|
-
"grok",
|
|
55
|
-
"antigravity",
|
|
56
|
-
] as const;
|
|
14
|
+
const MODEL_KEYS = ["codex", "claude-code", "opencode-go", "opencode", "cursor"] as const;
|
|
57
15
|
type ModelKey = (typeof MODEL_KEYS)[number];
|
|
58
16
|
|
|
59
17
|
export function billingKeyOf(r: Rung): BillingKey {
|
|
@@ -63,38 +21,25 @@ export function billingKeyOf(r: Rung): BillingKey {
|
|
|
63
21
|
|
|
64
22
|
const modelKeyOf = (k: BillingKey): ModelKey => (k === "claude" ? "claude-code" : k);
|
|
65
23
|
|
|
66
|
-
const BackendModelSchema = z.object({
|
|
67
|
-
id: z.string().min(1),
|
|
68
|
-
efforts: z.array(z.string().min(1)),
|
|
69
|
-
context: z.number().int().positive(),
|
|
70
|
-
});
|
|
24
|
+
const BackendModelSchema = z.object({ id: z.string().min(1) });
|
|
71
25
|
|
|
72
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* What catherd knows of a model that Slopalytics does not: its capabilities, how a plan weighs it and the
|
|
28
|
+
* model id each backend runs it under. `vendor` and `status` stay in the data as documentation.
|
|
29
|
+
*/
|
|
73
30
|
const FamilySchema = z.looseObject({
|
|
74
31
|
id: z.string().min(1),
|
|
75
32
|
name: z.string(),
|
|
76
33
|
capabilities: z.object({ toolUse: z.boolean(), imageIn: z.boolean(), reasoning: z.boolean() }),
|
|
77
|
-
/** API list price, dollars per million tokens */
|
|
78
|
-
price: z.object({ input: z.number(), cached: z.number(), output: z.number() }),
|
|
79
34
|
/** chatgpt-plan: messages per 5 hours relative to GPT-6 Luna (spec §5.3) */
|
|
80
35
|
planWeight: z.number().positive().optional(),
|
|
81
36
|
/** claude-plan: billed as usage credits, not from the plan's limits */
|
|
82
37
|
meteredOnPlan: z.boolean().default(false),
|
|
83
38
|
on: z.partialRecord(z.enum(MODEL_KEYS), BackendModelSchema),
|
|
84
39
|
notes: z.record(z.string(), z.string()).default({}),
|
|
85
|
-
/** the day the vendor released it (models.dev), a stand-in feature (spec 1.2 §6.3) */
|
|
86
|
-
releaseDate: z.iso.date().optional(),
|
|
87
|
-
/**
|
|
88
|
-
* spec 1.5 plan 24: the family this one succeeds in the same line (GPT-6.1 Sol after GPT-6 Sol): until a
|
|
89
|
-
* value of its own arrives, a rung takes at least its predecessor's value at the same effort
|
|
90
|
-
*/
|
|
91
|
-
predecessor: z.string().min(1).optional(),
|
|
92
|
-
/** a sync's speed facts (spec 1.2 §4.1), `<source>.<field>` → value; they never carry a bar */
|
|
93
|
-
speed: z.record(z.string(), z.number()).optional(),
|
|
94
40
|
});
|
|
95
41
|
export type Family = z.infer<typeof FamilySchema>;
|
|
96
42
|
|
|
97
|
-
/** `sources` (where the data came from) stays in the file as documentation; nothing reads it. */
|
|
98
43
|
export const ModelsFileSchema = z.looseObject({
|
|
99
44
|
schema: z.literal(1),
|
|
100
45
|
version: z.string(),
|
|
@@ -108,242 +53,62 @@ export const ModelsFileSchema = z.looseObject({
|
|
|
108
53
|
});
|
|
109
54
|
export type ModelsFile = z.infer<typeof ModelsFileSchema>;
|
|
110
55
|
|
|
111
|
-
|
|
112
|
-
export const CanonicalRung = z.string().regex(/^[^:#\s]+#[^#\s]+$/, "a canonical rung is model#effort");
|
|
113
|
-
|
|
114
|
-
/** Spec 1.2 §4.2: `anchor = a·x + b`, fitted on `n` rungs both sources cover, with its R². */
|
|
115
|
-
export const FitSchema = z.object({
|
|
116
|
-
source: z.string(),
|
|
117
|
-
field: z.string(),
|
|
118
|
-
a: z.number(),
|
|
119
|
-
b: z.number(),
|
|
120
|
-
r2: z.number(),
|
|
121
|
-
n: z.number().int(),
|
|
122
|
-
});
|
|
123
|
-
export type Fit = z.infer<typeof FitSchema>;
|
|
124
|
-
|
|
125
|
-
export const ScoreSchema = z.object({
|
|
126
|
-
rung: CanonicalRung,
|
|
127
|
-
dim: z.enum(DIMS),
|
|
128
|
-
value: z.number(),
|
|
129
|
-
benchmark: z.string(),
|
|
130
|
-
version: z.string(),
|
|
131
|
-
url: z.url(),
|
|
132
|
-
date: z.iso.date(),
|
|
133
|
-
confidence: z.enum(CONFIDENCE),
|
|
134
|
-
note: z.string().optional(),
|
|
135
|
-
/** spec 1.2 §4.3: the source a synced value came from (absent: the shipped file or the user's override) */
|
|
136
|
-
source: z.string().optional(),
|
|
137
|
-
/** a calibrated value: the fit that mapped it onto the anchor */
|
|
138
|
-
fit: FitSchema.optional(),
|
|
139
|
-
/** the source named no effort, so the family's default effort was assumed (spec 1.2 §3.4) */
|
|
140
|
-
effortAssumed: z.boolean().optional(),
|
|
141
|
-
});
|
|
142
|
-
export type Score = z.infer<typeof ScoreSchema>;
|
|
143
|
-
|
|
144
|
-
const BarSchema = z.partialRecord(z.enum(DIMS), z.number());
|
|
145
|
-
type Bars = Record<Kind, Record<Difficulty, Partial<Record<Dim, number>>>>;
|
|
146
|
-
const BarsSchema = z.record(z.enum(KINDS), z.record(z.enum(DIFFICULTIES), BarSchema));
|
|
147
|
-
|
|
148
|
-
/** Spec 1.2 §5.2: per dimension and difficulty, where the default threshold came from. */
|
|
149
|
-
const BarsWhySchema = z.partialRecord(z.enum(DIMS), z.partialRecord(z.enum(DIFFICULTIES), z.string()));
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* `catalog/scores.json`: the hand-typed and keyless values (spec 1.2 §7), the shipped treat-likes, and the
|
|
153
|
-
* default bars with the `barsWhy` line of each threshold (spec 1.2 §5.2).
|
|
154
|
-
*/
|
|
155
|
-
export const ScoresFileSchema = z.looseObject({
|
|
156
|
-
schema: z.literal(1),
|
|
157
|
-
version: z.string(),
|
|
158
|
-
benchmarks: z.record(z.enum(DIMS), z.object({ benchmark: z.string(), version: z.string() })),
|
|
159
|
-
scores: z.array(ScoreSchema),
|
|
160
|
-
treatLike: z.record(CanonicalRung, z.object({ like: CanonicalRung, note: z.string() })),
|
|
161
|
-
bars: BarsSchema,
|
|
162
|
-
barsWhy: BarsWhySchema.default({}),
|
|
163
|
-
});
|
|
164
|
-
export type ScoresFile = z.infer<typeof ScoresFileSchema>;
|
|
165
|
-
|
|
166
|
-
/** An override's bar: per dimension a threshold, or `null` to remove the default's (spec 1.2 §5.2). */
|
|
167
|
-
const OverrideBarSchema = z.partialRecord(z.enum(DIMS), z.number().nullable());
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* `<config>/catalog.override.json` (spec §3.5): the user's treat-likes, scores and bars. Its bars override
|
|
171
|
-
* the default bars per dimension (spec 1.2 §5.2): a number sets that threshold, `null` removes it, and a
|
|
172
|
-
* dimension it does not name keeps the default's.
|
|
173
|
-
*/
|
|
174
|
-
export const OverrideSchema = z.looseObject({
|
|
175
|
-
schema: z.literal(1).default(1),
|
|
176
|
-
treatLike: z.record(CanonicalRung, CanonicalRung).default({}),
|
|
177
|
-
scores: z.array(ScoreSchema).default([]),
|
|
178
|
-
bars: z.partialRecord(z.enum(KINDS), z.partialRecord(z.enum(DIFFICULTIES), OverrideBarSchema)).default({}),
|
|
179
|
-
});
|
|
180
|
-
export type Override = z.infer<typeof OverrideSchema>;
|
|
181
|
-
|
|
182
|
-
/** Spec 1.2 §6.1: the rung whose value a rung without one uses on a dimension, as `inferred`. */
|
|
183
|
-
export interface InferredStandIn {
|
|
184
|
-
like: string;
|
|
185
|
-
/** spec 1.2 §6.3's distance between the two */
|
|
186
|
-
distance: number;
|
|
187
|
-
/** the features that distance rests on */
|
|
188
|
-
features: string[];
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
interface Listed {
|
|
56
|
+
export interface Listed {
|
|
192
57
|
id: string;
|
|
193
58
|
efforts: string[];
|
|
194
59
|
context: number | null;
|
|
195
60
|
imageIn: boolean;
|
|
196
61
|
}
|
|
197
62
|
|
|
198
|
-
/**
|
|
63
|
+
/** One model at one effort on the Slopalytics index: the one number routing ranks on, and what it costs and does. */
|
|
64
|
+
export interface Pair {
|
|
65
|
+
intelligence: number;
|
|
66
|
+
price: { input: number; output: number } | null;
|
|
67
|
+
speed: number | null;
|
|
68
|
+
context: number | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Everything routing reads: the shipped families, the Slopalytics index, and each backend's listing. */
|
|
199
72
|
export interface Catalog {
|
|
200
73
|
families: Family[];
|
|
201
74
|
backends: ModelsFile["backends"];
|
|
202
|
-
/**
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
|
|
75
|
+
/** Slopalytics family (`slopKey`) → effort → pair */
|
|
76
|
+
index: Record<string, Record<string, Pair>>;
|
|
77
|
+
/** when the proxy fetched the index; null with no index */
|
|
78
|
+
indexedAt: string | null;
|
|
79
|
+
/** ascending intelligences of every indexed pair of the shipped families: what the bars are percentiles of */
|
|
80
|
+
pool: number[];
|
|
208
81
|
/** per backend id, the last `listModels()`; absent when never listed */
|
|
209
82
|
listed: Record<string, { fetchedAt: string; models: Listed[] }>;
|
|
210
|
-
/** `<canonical rung>|<kind or *>` → median seconds, only with ≥ 5 samples (spec §5.2) */
|
|
211
|
-
secs: Record<string, number>;
|
|
212
|
-
/** canonical rung → the Artificial Analysis stand-in features a sync read for it (spec 1.2 §6.3) */
|
|
213
|
-
features: Record<string, Record<string, number>>;
|
|
214
|
-
/**
|
|
215
|
-
* canonical rung → per dimension it has no value for (of its own or through a treat-like), the stand-in
|
|
216
|
-
* whose value it uses as `inferred` (spec 1.2 §6.1); filled by the stand-in ranking (services/standins.ts)
|
|
217
|
-
*/
|
|
218
|
-
inferred: Record<string, Partial<Record<Dim, InferredStandIn>>>;
|
|
219
|
-
/** the thresholds the user's override set or removed, as `<kind>/<difficulty>/<dim>` (route says "your override") */
|
|
220
|
-
userBars?: string[];
|
|
221
83
|
}
|
|
222
84
|
|
|
223
|
-
/**
|
|
224
|
-
* Spec 1.2 §3.5: what a sync learned about a family. `price`, `capabilities` and the opencode backends'
|
|
225
|
-
* `efforts` and `context` come from models.dev; `speed` holds facts that never carry a bar (spec 1.2 §4.1),
|
|
226
|
-
* as `<source>.<field>` → value.
|
|
227
|
-
*/
|
|
228
|
-
export const FamilyFactsSchema = z.object({
|
|
229
|
-
price: z.object({ input: z.number(), cached: z.number(), output: z.number() }).optional(),
|
|
230
|
-
capabilities: z.object({ toolUse: z.boolean(), imageIn: z.boolean(), reasoning: z.boolean() }).optional(),
|
|
231
|
-
on: z
|
|
232
|
-
.partialRecord(
|
|
233
|
-
z.enum(MODEL_KEYS),
|
|
234
|
-
z.object({ efforts: z.array(z.string()).optional(), context: z.number().int().positive().optional() }),
|
|
235
|
-
)
|
|
236
|
-
.default({}),
|
|
237
|
-
releaseDate: z.string().optional(),
|
|
238
|
-
speed: z.record(z.string(), z.number()).default({}),
|
|
239
|
-
});
|
|
240
|
-
export type FamilyFacts = z.infer<typeof FamilyFactsSchema>;
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Spec 1.2 §3.5: the families with a sync's facts laid over them. The shipped file stays the floor: a fact
|
|
244
|
-
* the sync lacks keeps the shipped one, and a backend keeps every effort it ships with.
|
|
245
|
-
*/
|
|
246
|
-
export function applyFacts(families: Family[], facts: Record<string, FamilyFacts>): Family[] {
|
|
247
|
-
return families.map((f) => {
|
|
248
|
-
const x = facts[f.id];
|
|
249
|
-
if (!x) return f;
|
|
250
|
-
const on = { ...f.on };
|
|
251
|
-
for (const key of MODEL_KEYS) {
|
|
252
|
-
const cur = on[key];
|
|
253
|
-
const got = x.on[key];
|
|
254
|
-
if (!cur || !got) continue;
|
|
255
|
-
on[key] = {
|
|
256
|
-
...cur,
|
|
257
|
-
efforts: [...cur.efforts, ...(got.efforts ?? []).filter((e) => !cur.efforts.includes(e))],
|
|
258
|
-
context: got.context ?? cur.context,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
// a sync may add a capability, never remove one the shipped file gives (the floor)
|
|
262
|
-
const got = x.capabilities;
|
|
263
|
-
const capabilities = got
|
|
264
|
-
? {
|
|
265
|
-
toolUse: f.capabilities.toolUse || got.toolUse,
|
|
266
|
-
imageIn: f.capabilities.imageIn || got.imageIn,
|
|
267
|
-
reasoning: f.capabilities.reasoning || got.reasoning,
|
|
268
|
-
}
|
|
269
|
-
: f.capabilities;
|
|
270
|
-
return {
|
|
271
|
-
...f,
|
|
272
|
-
price: x.price ?? f.price,
|
|
273
|
-
capabilities,
|
|
274
|
-
on,
|
|
275
|
-
...(x.releaseDate || f.releaseDate ? { releaseDate: x.releaseDate ?? f.releaseDate } : {}),
|
|
276
|
-
...(Object.keys(x.speed).length ? { speed: x.speed } : {}),
|
|
277
|
-
};
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/** Spec 1.2 §4.3: the level a value counts at: its own, one lower once it is older than STALE_DAYS. */
|
|
282
|
-
export function effectiveRank(s: Score, now: number): number {
|
|
283
|
-
const stale = now - Date.parse(s.date) > STALE_DAYS * DAY_MS;
|
|
284
|
-
return Math.min(RANK[s.confidence] + (stale ? 1 : 0), CONFIDENCE.length - 1);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** Spec 1.2 §4.3: `a` beats `b` at a better level, or at the same level with a newer date. */
|
|
288
|
-
export function outranks(a: Score, b: Score, now: number): boolean {
|
|
289
|
-
const ra = effectiveRank(a, now);
|
|
290
|
-
const rb = effectiveRank(b, now);
|
|
291
|
-
return ra < rb || (ra === rb && a.date > b.date);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* The catalog routing reads: the shipped files, the synced values (spec 1.2 §3), each backend's listing and
|
|
296
|
-
* the user's override, which always wins. Between shipped and synced values the better level wins, then the
|
|
297
|
-
* newer date (spec 1.2 §4.3); `now` dates the 90-day drop.
|
|
298
|
-
*/
|
|
299
85
|
export function buildCatalog(o: {
|
|
300
86
|
models: ModelsFile;
|
|
301
|
-
|
|
302
|
-
synced?: Score[];
|
|
303
|
-
facts?: Record<string, FamilyFacts>;
|
|
304
|
-
override?: Override;
|
|
87
|
+
index: IndexFile | null;
|
|
305
88
|
listed?: Catalog["listed"];
|
|
306
|
-
secs?: Catalog["secs"];
|
|
307
|
-
features?: Catalog["features"];
|
|
308
|
-
now?: number;
|
|
309
89
|
}): Catalog {
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
treatLike[rung] = { like, source: "user" };
|
|
326
|
-
const bars = structuredClone(o.scores.bars) as Bars;
|
|
327
|
-
const userBars: string[] = [];
|
|
328
|
-
for (const kind of KINDS)
|
|
329
|
-
for (const d of DIFFICULTIES)
|
|
330
|
-
for (const [dim, min] of Object.entries(o.override?.bars[kind]?.[d] ?? {}) as [Dim, number | null][]) {
|
|
331
|
-
userBars.push(`${kind}/${d}/${dim}`);
|
|
332
|
-
if (min === null) delete bars[kind][d][dim];
|
|
333
|
-
else bars[kind][d][dim] = min;
|
|
334
|
-
}
|
|
90
|
+
const index: Catalog["index"] = {};
|
|
91
|
+
for (const r of o.index?.models ?? [])
|
|
92
|
+
(index[slopKey(r.family)] ??= {})[r.effort] = {
|
|
93
|
+
intelligence: r.intelligence,
|
|
94
|
+
price:
|
|
95
|
+
r.inputPrice !== null && r.outputPrice !== null
|
|
96
|
+
? { input: r.inputPrice, output: r.outputPrice }
|
|
97
|
+
: null,
|
|
98
|
+
speed: r.speed,
|
|
99
|
+
context: r.context,
|
|
100
|
+
};
|
|
101
|
+
const shippedKeys = new Set(o.models.families.map((f) => slopKey(f.id)));
|
|
102
|
+
const pool = [...shippedKeys]
|
|
103
|
+
.flatMap((k) => Object.values(index[k] ?? {}).map((p) => p.intelligence))
|
|
104
|
+
.sort((a, b) => a - b);
|
|
335
105
|
return {
|
|
336
|
-
families: o.
|
|
106
|
+
families: o.models.families,
|
|
337
107
|
backends: o.models.backends,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
barsWhy: o.scores.barsWhy,
|
|
108
|
+
index,
|
|
109
|
+
indexedAt: o.index?.fetchedAt ?? null,
|
|
110
|
+
pool,
|
|
342
111
|
listed: o.listed ?? {},
|
|
343
|
-
secs: o.secs ?? {},
|
|
344
|
-
features: o.features ?? {},
|
|
345
|
-
inferred: {},
|
|
346
|
-
userBars,
|
|
347
112
|
};
|
|
348
113
|
}
|
|
349
114
|
|
|
@@ -353,12 +118,16 @@ export interface RungInfo {
|
|
|
353
118
|
parsed: Rung;
|
|
354
119
|
key: BillingKey;
|
|
355
120
|
family: Family | null;
|
|
356
|
-
/** `<family id or backend model id>#<effort>`: the key
|
|
121
|
+
/** `<family id or backend model id>#<effort>`: the key run evidence uses */
|
|
357
122
|
canonical: string;
|
|
123
|
+
/** the model's Slopalytics family key */
|
|
124
|
+
slop: string;
|
|
358
125
|
efforts: string[];
|
|
359
126
|
context: number | null;
|
|
360
127
|
/** true when the backend's last listing has it, false when that listing lacks it, null with no listing */
|
|
361
128
|
listed: boolean | null;
|
|
129
|
+
/** the rung's index entry; null when it is not indexed */
|
|
130
|
+
pair: Pair | null;
|
|
362
131
|
}
|
|
363
132
|
|
|
364
133
|
function familyOf(c: Catalog, r: Rung): Family | null {
|
|
@@ -369,67 +138,42 @@ function familyOf(c: Catalog, r: Rung): Family | null {
|
|
|
369
138
|
/** The backend's listing; the native `claude` backend has none of its own: it runs claude-code's models. */
|
|
370
139
|
const listingOf = (c: Catalog, backend: string) => c.listed[backend === "claude" ? "claude-code" : backend];
|
|
371
140
|
|
|
372
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* A rung's catalog facts; `rung` must parse (E_ADMIT_RUNG otherwise). Its pair is the index's record for the
|
|
143
|
+
* model's Slopalytics family at its effort; `#default` takes the family's `default` record, else its only one.
|
|
144
|
+
*/
|
|
373
145
|
export function rungInfo(c: Catalog, rung: string): RungInfo {
|
|
374
146
|
const parsed = parseRung(rung);
|
|
375
147
|
const key = billingKeyOf(parsed);
|
|
376
148
|
const family = familyOf(c, parsed);
|
|
377
|
-
const
|
|
149
|
+
const slop = slopKey(family?.id ?? parsed.model);
|
|
378
150
|
const listing = listingOf(c, parsed.backend);
|
|
379
151
|
const found = listing?.models.find((m) => m.id === parsed.model);
|
|
152
|
+
const indexed = c.index[slop] ?? {};
|
|
153
|
+
const only = Object.values(indexed);
|
|
154
|
+
const pair =
|
|
155
|
+
indexed[parsed.effort] ?? (parsed.effort === "default" && only.length === 1 ? (only[0] as Pair) : null);
|
|
380
156
|
return {
|
|
381
157
|
rung,
|
|
382
158
|
parsed,
|
|
383
159
|
key,
|
|
384
160
|
family,
|
|
385
161
|
canonical: `${family?.id ?? parsed.model}#${parsed.effort}`,
|
|
386
|
-
|
|
387
|
-
|
|
162
|
+
slop,
|
|
163
|
+
efforts: found?.efforts ?? Object.keys(indexed),
|
|
164
|
+
context: found?.context ?? pair?.context ?? null,
|
|
388
165
|
listed: listing && listing.models.length > 0 ? found !== undefined : null,
|
|
166
|
+
pair,
|
|
389
167
|
};
|
|
390
168
|
}
|
|
391
169
|
|
|
392
|
-
/**
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
export function scoresOf(
|
|
399
|
-
c: Catalog,
|
|
400
|
-
canonical: string,
|
|
401
|
-
): {
|
|
402
|
-
values: Partial<Record<Dim, number>>;
|
|
403
|
-
records: Partial<Record<Dim, Score>>;
|
|
404
|
-
via: string | null;
|
|
405
|
-
borrowed: Dim[];
|
|
406
|
-
inferred: Dim[];
|
|
407
|
-
standIns: Partial<Record<Dim, string>>;
|
|
408
|
-
} | null {
|
|
409
|
-
const own = c.scores[canonical] ?? {};
|
|
410
|
-
const like = c.treatLike[canonical]?.like ?? null;
|
|
411
|
-
const lent = like ? (c.scores[like] ?? {}) : {};
|
|
412
|
-
const guessed = c.inferred[canonical] ?? {};
|
|
413
|
-
const values: Partial<Record<Dim, number>> = {};
|
|
414
|
-
const records: Partial<Record<Dim, Score>> = {};
|
|
415
|
-
const borrowed: Dim[] = [];
|
|
416
|
-
const inferred: Dim[] = [];
|
|
417
|
-
const standIns: Partial<Record<Dim, string>> = {};
|
|
418
|
-
for (const d of DIMS) {
|
|
419
|
-
const stand = guessed[d];
|
|
420
|
-
const r = own[d] ?? lent[d] ?? (stand ? c.scores[stand.like]?.[d] : undefined);
|
|
421
|
-
if (!r) continue;
|
|
422
|
-
records[d] = r;
|
|
423
|
-
values[d] = r.value;
|
|
424
|
-
if (own[d]) continue;
|
|
425
|
-
if (lent[d]) borrowed.push(d);
|
|
426
|
-
else if (stand) {
|
|
427
|
-
inferred.push(d);
|
|
428
|
-
standIns[d] = stand.like;
|
|
429
|
-
}
|
|
170
|
+
/** The rung's index entry; null when it is not a rung or not indexed. */
|
|
171
|
+
export function pairOf(c: Catalog, rung: string): Pair | null {
|
|
172
|
+
try {
|
|
173
|
+
return rungInfo(c, rung).pair;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
430
176
|
}
|
|
431
|
-
if (Object.keys(records).length === 0) return null;
|
|
432
|
-
return { values, records, via: borrowed.length ? like : null, borrowed, inferred, standIns };
|
|
433
177
|
}
|
|
434
178
|
|
|
435
179
|
/** Spec §4 roles: what a rung must offer to be placed on a role. */
|
|
@@ -442,6 +186,7 @@ export const ROLE_NEEDS: Record<Role, { toolUse?: true; imageIn?: true; imageGen
|
|
|
442
186
|
artist: { imageGen: true },
|
|
443
187
|
writer: { toolUse: true },
|
|
444
188
|
researcher: { toolUse: true },
|
|
189
|
+
judge: { toolUse: true },
|
|
445
190
|
};
|
|
446
191
|
|
|
447
192
|
/**
|
package/src/domain/cost.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BillingKey, Family } from "./catalog.ts";
|
|
1
|
+
import type { BillingKey, Family, Pair } from "./catalog.ts";
|
|
2
2
|
|
|
3
3
|
/** Spec §5.3: how the user pays for each billing key. */
|
|
4
4
|
export const BILLING_MODES = ["chatgpt-plan", "claude-plan", "subscription", "metered"] as const;
|
|
@@ -12,8 +12,6 @@ export const DEFAULT_BILLING: Record<BillingKey, BillingMode> = {
|
|
|
12
12
|
"opencode-go": "subscription",
|
|
13
13
|
opencode: "metered",
|
|
14
14
|
cursor: "metered",
|
|
15
|
-
grok: "metered",
|
|
16
|
-
antigravity: "metered",
|
|
17
15
|
};
|
|
18
16
|
|
|
19
17
|
/**
|
|
@@ -38,10 +36,10 @@ const REF_TASK = { input: 60_000, cached: 400_000, output: 18_000 };
|
|
|
38
36
|
|
|
39
37
|
export const effortFactor = (effort: string): number => EFFORT_FACTOR[effort] ?? 1;
|
|
40
38
|
|
|
41
|
-
/** Estimated list-price dollars for one task at `effort
|
|
42
|
-
export function taskUsd(price:
|
|
39
|
+
/** Estimated list-price dollars for one task at `effort`; cached input costs a tenth of the input price. */
|
|
40
|
+
export function taskUsd(price: { input: number; output: number }, effort: string): number {
|
|
43
41
|
const perTask =
|
|
44
|
-
REF_TASK.input * price.input + REF_TASK.cached * price.
|
|
42
|
+
REF_TASK.input * price.input + REF_TASK.cached * (price.input / 10) + REF_TASK.output * price.output;
|
|
45
43
|
return (perTask / 1_000_000) * effortFactor(effort);
|
|
46
44
|
}
|
|
47
45
|
|
|
@@ -60,31 +58,18 @@ export interface Cost {
|
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
/**
|
|
63
|
-
* Spec §5.3. Every mode yields list-price dollars per task, so plans compare with each other.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* Go's share of its monthly dollar limit is dollars / limit, which orders the same as dollars.
|
|
61
|
+
* Spec §5.3. Every mode yields list-price dollars per task, so plans compare with each other. chatgpt-plan scales
|
|
62
|
+
* the official quota weight (Luna 1, Sol 20, Astra 60) by the effort factor; claude-plan uses API prices as the
|
|
63
|
+
* proxy, with Fable metered (catherd cannot tell Pro from Max); Go's share of its monthly dollar limit is
|
|
64
|
+
* dollars / limit, which orders the same as dollars. A pair with no price costs 0 within its tier when a plan or
|
|
65
|
+
* subscription pays for it, so it can start a lane; a metered one stays unknown, last in its tier.
|
|
69
66
|
*/
|
|
70
|
-
export function costOf(family: Family | null, effort: string, mode: BillingMode): Cost {
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
tier: 0,
|
|
77
|
-
value:
|
|
78
|
-
family.planWeight === undefined ? usd : family.planWeight * CHATGPT_UNIT_USD * effortFactor(effort),
|
|
79
|
-
mode,
|
|
80
|
-
};
|
|
81
|
-
case "claude-plan":
|
|
82
|
-
return { tier: family.meteredOnPlan ? 1 : 0, value: usd, mode };
|
|
83
|
-
case "subscription":
|
|
84
|
-
return { tier: 0, value: usd, mode };
|
|
85
|
-
case "metered":
|
|
86
|
-
return { tier: 1, value: usd, mode };
|
|
87
|
-
}
|
|
67
|
+
export function costOf(family: Family | null, pair: Pair | null, effort: string, mode: BillingMode): Cost {
|
|
68
|
+
if (mode === "chatgpt-plan" && family?.planWeight !== undefined)
|
|
69
|
+
return { tier: 0, value: family.planWeight * CHATGPT_UNIT_USD * effortFactor(effort), mode };
|
|
70
|
+
const metered = mode === "metered" || (mode === "claude-plan" && family?.meteredOnPlan === true);
|
|
71
|
+
if (!pair?.price) return metered ? { tier: 1, value: null, mode } : { tier: 0, value: 0, mode };
|
|
72
|
+
return { tier: metered ? 1 : 0, value: taskUsd(pair.price, effort), mode };
|
|
88
73
|
}
|
|
89
74
|
|
|
90
75
|
/** Cheaper first: tier, then value, an unknown value last. */
|
package/src/domain/errors.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export type ErrorCode =
|
|
2
2
|
| "E_CONFIG_INVALID"
|
|
3
3
|
| "E_CONFIG_NEWER_SCHEMA"
|
|
4
|
-
| "E_CONFIG_KEYBIND"
|
|
5
4
|
| "E_BACKEND_MISSING"
|
|
6
5
|
| "E_BACKEND_TOO_OLD"
|
|
7
6
|
| "E_BACKEND_NOT_LOGGED_IN"
|
|
@@ -21,6 +20,7 @@ export type ErrorCode =
|
|
|
21
20
|
| "E_ADMIT_ORDER"
|
|
22
21
|
| "E_LANE_INVALID"
|
|
23
22
|
| "E_LAND_GATE"
|
|
23
|
+
| "E_TREE"
|
|
24
24
|
| "E_CLIMB_DESIGN"
|
|
25
25
|
| "E_CLIMB_ENV"
|
|
26
26
|
| "E_JEV_KEY"
|
|
@@ -35,7 +35,7 @@ export type ErrorCode =
|
|
|
35
35
|
| "E_RUNTIME_TOO_OLD";
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* How to get a newer catherd. `bunx catherd-cli@latest` alone
|
|
38
|
+
* How to get a newer catherd. `bunx catherd-cli@latest` alone prints the newest one's help and upgrades
|
|
39
39
|
* nothing that is installed (audit N12).
|
|
40
40
|
*/
|
|
41
41
|
export const UPGRADE = "bun add -g catherd-cli@latest (or run bunx catherd-cli@latest <command>)";
|