catherd-cli 1.4.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 +188 -169
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -239
- 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 -356
- package/plugin/skills/catherd-setup/SKILL.md +21 -20
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +19 -4
- package/src/adapters/claude-code/index.ts +126 -14
- package/src/adapters/codex/events.ts +10 -0
- package/src/adapters/codex/index.ts +17 -5
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +10 -4
- package/src/adapters/opencode/index.ts +38 -1
- package/src/cli.ts +27 -27
- package/src/domain/agents.ts +3 -1
- 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 +106 -0
- package/src/domain/catalog.ts +69 -313
- package/src/domain/changes.ts +23 -1
- package/src/domain/cost.ts +19 -29
- package/src/domain/errors.ts +6 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -75
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +53 -0
- package/src/domain/hints.ts +13 -2
- 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 +118 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +32 -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 +175 -138
- package/src/domain/profile.ts +140 -63
- package/src/domain/record.ts +68 -3
- package/src/domain/reply.ts +29 -0
- package/src/domain/role-prompts.ts +182 -56
- package/src/domain/role-scope.ts +76 -0
- package/src/domain/role-tools.ts +34 -0
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +90 -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 +229 -72
- package/src/domain/slopalytics.ts +72 -0
- package/src/domain/state.ts +5 -0
- package/src/domain/util.ts +22 -0
- package/src/domain/workspace.ts +130 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +24 -165
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +18 -2
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/deps.ts +2 -0
- package/src/entry/doctor-command.ts +20 -2
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/host-arg.ts +28 -0
- package/src/entry/init-command.ts +20 -69
- package/src/entry/knowledge-command.ts +121 -5
- package/src/entry/lock-command.ts +103 -8
- package/src/entry/mcp/handshake.ts +6 -2
- package/src/entry/mcp/role-bin.ts +20 -0
- package/src/entry/mcp/role-server.ts +134 -0
- package/src/entry/mcp/server.ts +85 -21
- 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/pause-command.ts +67 -0
- package/src/entry/profile-command.ts +33 -18
- package/src/entry/role-cli-command.ts +159 -0
- package/src/entry/runs-command.ts +81 -27
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/entry/supervise-command.ts +30 -3
- package/src/entry/workspace-command.ts +44 -0
- package/src/infra/boot-lock.ts +25 -0
- package/src/infra/delivery.ts +3 -0
- package/src/infra/dispatch-dir.ts +2 -0
- package/src/infra/env.ts +11 -3
- package/src/infra/git.ts +70 -0
- package/src/infra/heavy-lock.ts +133 -6
- package/src/infra/host-probe.ts +16 -0
- package/src/infra/launch.ts +63 -7
- package/src/infra/lock-activity.ts +88 -0
- package/src/infra/login-env.ts +48 -0
- 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 +37 -2
- package/src/infra/role-mcp.ts +69 -0
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/infra/supervisor.ts +107 -16
- package/src/services/admission.ts +133 -31
- package/src/services/backends.ts +4 -6
- package/src/services/budget.ts +26 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -254
- package/src/services/credentials.ts +15 -26
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +209 -17
- package/src/services/dispatches.ts +30 -2
- package/src/services/doctor-access.ts +8 -3
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor-docker.ts +221 -0
- package/src/services/doctor-role-mcp.ts +65 -0
- package/src/services/doctor.ts +72 -15
- package/src/services/end-push.ts +157 -0
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +24 -2
- package/src/services/gate-env.ts +217 -0
- package/src/services/gate-service.ts +231 -45
- package/src/services/global-install.ts +46 -13
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-edit.ts +154 -0
- package/src/services/lane-service.ts +291 -55
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +167 -29
- package/src/services/notifier.ts +124 -4
- package/src/services/notify-service.ts +26 -0
- package/src/services/orchestrator-wait.ts +82 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/pause.ts +161 -0
- package/src/services/peek.ts +79 -17
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +60 -5
- package/src/services/preflight.ts +232 -24
- package/src/services/profile-service.ts +31 -3
- package/src/services/profile-store.ts +18 -6
- package/src/services/protocol.ts +123 -25
- package/src/services/provenance.ts +34 -89
- package/src/services/questions.ts +24 -0
- package/src/services/reconcile.ts +33 -1
- package/src/services/reentry.ts +7 -6
- package/src/services/role-access.ts +27 -0
- package/src/services/routing-service.ts +161 -42
- 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-pin.ts +125 -0
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +204 -23
- package/src/services/run-store.ts +159 -18
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/scratch.ts +60 -0
- package/src/services/sessions.ts +3 -1
- package/src/services/state.ts +21 -0
- package/src/services/summary.ts +76 -12
- package/src/services/tree-path.ts +27 -0
- package/src/services/verifier-brief.ts +23 -0
- package/src/services/verifier-step.ts +48 -0
- package/src/services/workspace-admission.ts +259 -0
- package/src/services/workspace-service.ts +259 -0
- package/src/services/workspace-store.ts +95 -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 -36
- 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 -320
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -132
- package/src/adapters/grok/index.ts +0 -339
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -125
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -84
- package/src/entry/mcp/protocol-tools.ts +0 -57
- package/src/entry/mcp/run-tools.ts +0 -148
- package/src/entry/mcp/setup-tools.ts +0 -108
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -381
- 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 -373
- 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 -573
- package/src/entry/tui/views/save-dialog.tsx +0 -340
- package/src/entry/tui/views/status.tsx +0 -209
- 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 -87
- package/src/infra/sources/artificial-analysis.ts +0 -112
- package/src/infra/sources/cache.ts +0 -124
- 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 -255
- package/src/services/source-derive.ts +0 -374
- package/src/services/source-sync.ts +0 -305
- package/src/services/standins.ts +0 -204
- package/src/services/treat-likes.ts +0 -97
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,33 +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
|
-
/** a sync's speed facts (spec 1.2 §4.1), `<source>.<field>` → value; they never carry a bar */
|
|
88
|
-
speed: z.record(z.string(), z.number()).optional(),
|
|
89
40
|
});
|
|
90
41
|
export type Family = z.infer<typeof FamilySchema>;
|
|
91
42
|
|
|
92
|
-
/** `sources` (where the data came from) stays in the file as documentation; nothing reads it. */
|
|
93
43
|
export const ModelsFileSchema = z.looseObject({
|
|
94
44
|
schema: z.literal(1),
|
|
95
45
|
version: z.string(),
|
|
@@ -103,236 +53,62 @@ export const ModelsFileSchema = z.looseObject({
|
|
|
103
53
|
});
|
|
104
54
|
export type ModelsFile = z.infer<typeof ModelsFileSchema>;
|
|
105
55
|
|
|
106
|
-
|
|
107
|
-
export const CanonicalRung = z.string().regex(/^[^:#\s]+#[^#\s]+$/, "a canonical rung is model#effort");
|
|
108
|
-
|
|
109
|
-
/** Spec 1.2 §4.2: `anchor = a·x + b`, fitted on `n` rungs both sources cover, with its R². */
|
|
110
|
-
export const FitSchema = z.object({
|
|
111
|
-
source: z.string(),
|
|
112
|
-
field: z.string(),
|
|
113
|
-
a: z.number(),
|
|
114
|
-
b: z.number(),
|
|
115
|
-
r2: z.number(),
|
|
116
|
-
n: z.number().int(),
|
|
117
|
-
});
|
|
118
|
-
export type Fit = z.infer<typeof FitSchema>;
|
|
119
|
-
|
|
120
|
-
export const ScoreSchema = z.object({
|
|
121
|
-
rung: CanonicalRung,
|
|
122
|
-
dim: z.enum(DIMS),
|
|
123
|
-
value: z.number(),
|
|
124
|
-
benchmark: z.string(),
|
|
125
|
-
version: z.string(),
|
|
126
|
-
url: z.url(),
|
|
127
|
-
date: z.iso.date(),
|
|
128
|
-
confidence: z.enum(CONFIDENCE),
|
|
129
|
-
note: z.string().optional(),
|
|
130
|
-
/** spec 1.2 §4.3: the source a synced value came from (absent: the shipped file or the user's override) */
|
|
131
|
-
source: z.string().optional(),
|
|
132
|
-
/** a calibrated value: the fit that mapped it onto the anchor */
|
|
133
|
-
fit: FitSchema.optional(),
|
|
134
|
-
/** the source named no effort, so the family's default effort was assumed (spec 1.2 §3.4) */
|
|
135
|
-
effortAssumed: z.boolean().optional(),
|
|
136
|
-
});
|
|
137
|
-
export type Score = z.infer<typeof ScoreSchema>;
|
|
138
|
-
|
|
139
|
-
const BarSchema = z.partialRecord(z.enum(DIMS), z.number());
|
|
140
|
-
type Bars = Record<Kind, Record<Difficulty, Partial<Record<Dim, number>>>>;
|
|
141
|
-
const BarsSchema = z.record(z.enum(KINDS), z.record(z.enum(DIFFICULTIES), BarSchema));
|
|
142
|
-
|
|
143
|
-
/** Spec 1.2 §5.2: per dimension and difficulty, where the default threshold came from. */
|
|
144
|
-
const BarsWhySchema = z.partialRecord(z.enum(DIMS), z.partialRecord(z.enum(DIFFICULTIES), z.string()));
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* `catalog/scores.json`: the hand-typed and keyless values (spec 1.2 §7), the shipped treat-likes, and the
|
|
148
|
-
* default bars with the `barsWhy` line of each threshold (spec 1.2 §5.2).
|
|
149
|
-
*/
|
|
150
|
-
export const ScoresFileSchema = z.looseObject({
|
|
151
|
-
schema: z.literal(1),
|
|
152
|
-
version: z.string(),
|
|
153
|
-
benchmarks: z.record(z.enum(DIMS), z.object({ benchmark: z.string(), version: z.string() })),
|
|
154
|
-
scores: z.array(ScoreSchema),
|
|
155
|
-
treatLike: z.record(CanonicalRung, z.object({ like: CanonicalRung, note: z.string() })),
|
|
156
|
-
bars: BarsSchema,
|
|
157
|
-
barsWhy: BarsWhySchema.default({}),
|
|
158
|
-
});
|
|
159
|
-
export type ScoresFile = z.infer<typeof ScoresFileSchema>;
|
|
160
|
-
|
|
161
|
-
/** An override's bar: per dimension a threshold, or `null` to remove the default's (spec 1.2 §5.2). */
|
|
162
|
-
const OverrideBarSchema = z.partialRecord(z.enum(DIMS), z.number().nullable());
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* `<config>/catalog.override.json` (spec §3.5): the user's treat-likes, scores and bars. Its bars override
|
|
166
|
-
* the default bars per dimension (spec 1.2 §5.2): a number sets that threshold, `null` removes it, and a
|
|
167
|
-
* dimension it does not name keeps the default's.
|
|
168
|
-
*/
|
|
169
|
-
export const OverrideSchema = z.looseObject({
|
|
170
|
-
schema: z.literal(1).default(1),
|
|
171
|
-
treatLike: z.record(CanonicalRung, CanonicalRung).default({}),
|
|
172
|
-
scores: z.array(ScoreSchema).default([]),
|
|
173
|
-
bars: z.partialRecord(z.enum(KINDS), z.partialRecord(z.enum(DIFFICULTIES), OverrideBarSchema)).default({}),
|
|
174
|
-
});
|
|
175
|
-
export type Override = z.infer<typeof OverrideSchema>;
|
|
176
|
-
|
|
177
|
-
/** Spec 1.2 §6.1: the rung whose value a rung without one uses on a dimension, as `inferred`. */
|
|
178
|
-
export interface InferredStandIn {
|
|
179
|
-
like: string;
|
|
180
|
-
/** spec 1.2 §6.3's distance between the two */
|
|
181
|
-
distance: number;
|
|
182
|
-
/** the features that distance rests on */
|
|
183
|
-
features: string[];
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
interface Listed {
|
|
56
|
+
export interface Listed {
|
|
187
57
|
id: string;
|
|
188
58
|
efforts: string[];
|
|
189
59
|
context: number | null;
|
|
190
60
|
imageIn: boolean;
|
|
191
61
|
}
|
|
192
62
|
|
|
193
|
-
/**
|
|
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. */
|
|
194
72
|
export interface Catalog {
|
|
195
73
|
families: Family[];
|
|
196
74
|
backends: ModelsFile["backends"];
|
|
197
|
-
/**
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
|
|
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[];
|
|
203
81
|
/** per backend id, the last `listModels()`; absent when never listed */
|
|
204
82
|
listed: Record<string, { fetchedAt: string; models: Listed[] }>;
|
|
205
|
-
/** `<canonical rung>|<kind or *>` → median seconds, only with ≥ 5 samples (spec §5.2) */
|
|
206
|
-
secs: Record<string, number>;
|
|
207
|
-
/** canonical rung → the Artificial Analysis stand-in features a sync read for it (spec 1.2 §6.3) */
|
|
208
|
-
features: Record<string, Record<string, number>>;
|
|
209
|
-
/**
|
|
210
|
-
* canonical rung → per dimension it has no value for (of its own or through a treat-like), the stand-in
|
|
211
|
-
* whose value it uses as `inferred` (spec 1.2 §6.1); filled by the stand-in ranking (services/standins.ts)
|
|
212
|
-
*/
|
|
213
|
-
inferred: Record<string, Partial<Record<Dim, InferredStandIn>>>;
|
|
214
83
|
}
|
|
215
84
|
|
|
216
|
-
/**
|
|
217
|
-
* Spec 1.2 §3.5: what a sync learned about a family. `price`, `capabilities` and the opencode backends'
|
|
218
|
-
* `efforts` and `context` come from models.dev; `speed` holds facts that never carry a bar (spec 1.2 §4.1),
|
|
219
|
-
* as `<source>.<field>` → value.
|
|
220
|
-
*/
|
|
221
|
-
export const FamilyFactsSchema = z.object({
|
|
222
|
-
price: z.object({ input: z.number(), cached: z.number(), output: z.number() }).optional(),
|
|
223
|
-
capabilities: z.object({ toolUse: z.boolean(), imageIn: z.boolean(), reasoning: z.boolean() }).optional(),
|
|
224
|
-
on: z
|
|
225
|
-
.partialRecord(
|
|
226
|
-
z.enum(MODEL_KEYS),
|
|
227
|
-
z.object({ efforts: z.array(z.string()).optional(), context: z.number().int().positive().optional() }),
|
|
228
|
-
)
|
|
229
|
-
.default({}),
|
|
230
|
-
releaseDate: z.string().optional(),
|
|
231
|
-
speed: z.record(z.string(), z.number()).default({}),
|
|
232
|
-
});
|
|
233
|
-
export type FamilyFacts = z.infer<typeof FamilyFactsSchema>;
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Spec 1.2 §3.5: the families with a sync's facts laid over them. The shipped file stays the floor: a fact
|
|
237
|
-
* the sync lacks keeps the shipped one, and a backend keeps every effort it ships with.
|
|
238
|
-
*/
|
|
239
|
-
export function applyFacts(families: Family[], facts: Record<string, FamilyFacts>): Family[] {
|
|
240
|
-
return families.map((f) => {
|
|
241
|
-
const x = facts[f.id];
|
|
242
|
-
if (!x) return f;
|
|
243
|
-
const on = { ...f.on };
|
|
244
|
-
for (const key of MODEL_KEYS) {
|
|
245
|
-
const cur = on[key];
|
|
246
|
-
const got = x.on[key];
|
|
247
|
-
if (!cur || !got) continue;
|
|
248
|
-
on[key] = {
|
|
249
|
-
...cur,
|
|
250
|
-
efforts: [...cur.efforts, ...(got.efforts ?? []).filter((e) => !cur.efforts.includes(e))],
|
|
251
|
-
context: got.context ?? cur.context,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
// a sync may add a capability, never remove one the shipped file gives (the floor)
|
|
255
|
-
const got = x.capabilities;
|
|
256
|
-
const capabilities = got
|
|
257
|
-
? {
|
|
258
|
-
toolUse: f.capabilities.toolUse || got.toolUse,
|
|
259
|
-
imageIn: f.capabilities.imageIn || got.imageIn,
|
|
260
|
-
reasoning: f.capabilities.reasoning || got.reasoning,
|
|
261
|
-
}
|
|
262
|
-
: f.capabilities;
|
|
263
|
-
return {
|
|
264
|
-
...f,
|
|
265
|
-
price: x.price ?? f.price,
|
|
266
|
-
capabilities,
|
|
267
|
-
on,
|
|
268
|
-
...(x.releaseDate || f.releaseDate ? { releaseDate: x.releaseDate ?? f.releaseDate } : {}),
|
|
269
|
-
...(Object.keys(x.speed).length ? { speed: x.speed } : {}),
|
|
270
|
-
};
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/** Spec 1.2 §4.3: the level a value counts at: its own, one lower once it is older than STALE_DAYS. */
|
|
275
|
-
export function effectiveRank(s: Score, now: number): number {
|
|
276
|
-
const stale = now - Date.parse(s.date) > STALE_DAYS * DAY_MS;
|
|
277
|
-
return Math.min(RANK[s.confidence] + (stale ? 1 : 0), CONFIDENCE.length - 1);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** Spec 1.2 §4.3: `a` beats `b` at a better level, or at the same level with a newer date. */
|
|
281
|
-
export function outranks(a: Score, b: Score, now: number): boolean {
|
|
282
|
-
const ra = effectiveRank(a, now);
|
|
283
|
-
const rb = effectiveRank(b, now);
|
|
284
|
-
return ra < rb || (ra === rb && a.date > b.date);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* The catalog routing reads: the shipped files, the synced values (spec 1.2 §3), each backend's listing and
|
|
289
|
-
* the user's override, which always wins. Between shipped and synced values the better level wins, then the
|
|
290
|
-
* newer date (spec 1.2 §4.3); `now` dates the 90-day drop.
|
|
291
|
-
*/
|
|
292
85
|
export function buildCatalog(o: {
|
|
293
86
|
models: ModelsFile;
|
|
294
|
-
|
|
295
|
-
synced?: Score[];
|
|
296
|
-
facts?: Record<string, FamilyFacts>;
|
|
297
|
-
override?: Override;
|
|
87
|
+
index: IndexFile | null;
|
|
298
88
|
listed?: Catalog["listed"];
|
|
299
|
-
secs?: Catalog["secs"];
|
|
300
|
-
features?: Catalog["features"];
|
|
301
|
-
now?: number;
|
|
302
89
|
}): Catalog {
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
treatLike[rung] = { like, source: "user" };
|
|
319
|
-
const bars = structuredClone(o.scores.bars) as Bars;
|
|
320
|
-
for (const kind of KINDS)
|
|
321
|
-
for (const d of DIFFICULTIES)
|
|
322
|
-
for (const [dim, min] of Object.entries(o.override?.bars[kind]?.[d] ?? {}) as [Dim, number | null][])
|
|
323
|
-
if (min === null) delete bars[kind][d][dim];
|
|
324
|
-
else bars[kind][d][dim] = min;
|
|
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);
|
|
325
105
|
return {
|
|
326
|
-
families: o.
|
|
106
|
+
families: o.models.families,
|
|
327
107
|
backends: o.models.backends,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
barsWhy: o.scores.barsWhy,
|
|
108
|
+
index,
|
|
109
|
+
indexedAt: o.index?.fetchedAt ?? null,
|
|
110
|
+
pool,
|
|
332
111
|
listed: o.listed ?? {},
|
|
333
|
-
secs: o.secs ?? {},
|
|
334
|
-
features: o.features ?? {},
|
|
335
|
-
inferred: {},
|
|
336
112
|
};
|
|
337
113
|
}
|
|
338
114
|
|
|
@@ -342,12 +118,16 @@ export interface RungInfo {
|
|
|
342
118
|
parsed: Rung;
|
|
343
119
|
key: BillingKey;
|
|
344
120
|
family: Family | null;
|
|
345
|
-
/** `<family id or backend model id>#<effort>`: the key
|
|
121
|
+
/** `<family id or backend model id>#<effort>`: the key run evidence uses */
|
|
346
122
|
canonical: string;
|
|
123
|
+
/** the model's Slopalytics family key */
|
|
124
|
+
slop: string;
|
|
347
125
|
efforts: string[];
|
|
348
126
|
context: number | null;
|
|
349
127
|
/** true when the backend's last listing has it, false when that listing lacks it, null with no listing */
|
|
350
128
|
listed: boolean | null;
|
|
129
|
+
/** the rung's index entry; null when it is not indexed */
|
|
130
|
+
pair: Pair | null;
|
|
351
131
|
}
|
|
352
132
|
|
|
353
133
|
function familyOf(c: Catalog, r: Rung): Family | null {
|
|
@@ -358,67 +138,42 @@ function familyOf(c: Catalog, r: Rung): Family | null {
|
|
|
358
138
|
/** The backend's listing; the native `claude` backend has none of its own: it runs claude-code's models. */
|
|
359
139
|
const listingOf = (c: Catalog, backend: string) => c.listed[backend === "claude" ? "claude-code" : backend];
|
|
360
140
|
|
|
361
|
-
/**
|
|
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
|
+
*/
|
|
362
145
|
export function rungInfo(c: Catalog, rung: string): RungInfo {
|
|
363
146
|
const parsed = parseRung(rung);
|
|
364
147
|
const key = billingKeyOf(parsed);
|
|
365
148
|
const family = familyOf(c, parsed);
|
|
366
|
-
const
|
|
149
|
+
const slop = slopKey(family?.id ?? parsed.model);
|
|
367
150
|
const listing = listingOf(c, parsed.backend);
|
|
368
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);
|
|
369
156
|
return {
|
|
370
157
|
rung,
|
|
371
158
|
parsed,
|
|
372
159
|
key,
|
|
373
160
|
family,
|
|
374
161
|
canonical: `${family?.id ?? parsed.model}#${parsed.effort}`,
|
|
375
|
-
|
|
376
|
-
|
|
162
|
+
slop,
|
|
163
|
+
efforts: found?.efforts ?? Object.keys(indexed),
|
|
164
|
+
context: found?.context ?? pair?.context ?? null,
|
|
377
165
|
listed: listing && listing.models.length > 0 ? found !== undefined : null,
|
|
166
|
+
pair,
|
|
378
167
|
};
|
|
379
168
|
}
|
|
380
169
|
|
|
381
|
-
/**
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
export function scoresOf(
|
|
388
|
-
c: Catalog,
|
|
389
|
-
canonical: string,
|
|
390
|
-
): {
|
|
391
|
-
values: Partial<Record<Dim, number>>;
|
|
392
|
-
records: Partial<Record<Dim, Score>>;
|
|
393
|
-
via: string | null;
|
|
394
|
-
borrowed: Dim[];
|
|
395
|
-
inferred: Dim[];
|
|
396
|
-
standIns: Partial<Record<Dim, string>>;
|
|
397
|
-
} | null {
|
|
398
|
-
const own = c.scores[canonical] ?? {};
|
|
399
|
-
const like = c.treatLike[canonical]?.like ?? null;
|
|
400
|
-
const lent = like ? (c.scores[like] ?? {}) : {};
|
|
401
|
-
const guessed = c.inferred[canonical] ?? {};
|
|
402
|
-
const values: Partial<Record<Dim, number>> = {};
|
|
403
|
-
const records: Partial<Record<Dim, Score>> = {};
|
|
404
|
-
const borrowed: Dim[] = [];
|
|
405
|
-
const inferred: Dim[] = [];
|
|
406
|
-
const standIns: Partial<Record<Dim, string>> = {};
|
|
407
|
-
for (const d of DIMS) {
|
|
408
|
-
const stand = guessed[d];
|
|
409
|
-
const r = own[d] ?? lent[d] ?? (stand ? c.scores[stand.like]?.[d] : undefined);
|
|
410
|
-
if (!r) continue;
|
|
411
|
-
records[d] = r;
|
|
412
|
-
values[d] = r.value;
|
|
413
|
-
if (own[d]) continue;
|
|
414
|
-
if (lent[d]) borrowed.push(d);
|
|
415
|
-
else if (stand) {
|
|
416
|
-
inferred.push(d);
|
|
417
|
-
standIns[d] = stand.like;
|
|
418
|
-
}
|
|
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;
|
|
419
176
|
}
|
|
420
|
-
if (Object.keys(records).length === 0) return null;
|
|
421
|
-
return { values, records, via: borrowed.length ? like : null, borrowed, inferred, standIns };
|
|
422
177
|
}
|
|
423
178
|
|
|
424
179
|
/** Spec §4 roles: what a rung must offer to be placed on a role. */
|
|
@@ -431,6 +186,7 @@ export const ROLE_NEEDS: Record<Role, { toolUse?: true; imageIn?: true; imageGen
|
|
|
431
186
|
artist: { imageGen: true },
|
|
432
187
|
writer: { toolUse: true },
|
|
433
188
|
researcher: { toolUse: true },
|
|
189
|
+
judge: { toolUse: true },
|
|
434
190
|
};
|
|
435
191
|
|
|
436
192
|
/**
|
package/src/domain/changes.ts
CHANGED
|
@@ -33,6 +33,28 @@ export function changedPaths(before: Snapshot, after: Snapshot): string[] {
|
|
|
33
33
|
.sort();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** Spec 1.5 plan 21: a writer's implicit docs lane, when its brief names no Owns. */
|
|
37
|
+
export const DOCS_OWNS = ["docs/**", "*.md"];
|
|
38
|
+
|
|
39
|
+
/** The paths of an `Owns:` line in a brief (as a lane file writes it), or null when it has none. */
|
|
40
|
+
export function briefOwns(brief: string): string[] | null {
|
|
41
|
+
const line = /^Owns:\s*(.+)$/m.exec(brief)?.[1];
|
|
42
|
+
const paths = line
|
|
43
|
+
?.split(",")
|
|
44
|
+
.map((p) => p.trim().replace(/^`|`$/g, ""))
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
return paths?.length ? paths : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether `path` is inside an Owns entry: a path covers what is under it, and the other way round (as lanes'
|
|
51
|
+
* Owns always did); `dir/**` covers what is under dir; `*.ext` (no slash) is any file with that extension.
|
|
52
|
+
*/
|
|
53
|
+
export function ownsPath(path: string, entry: string): boolean {
|
|
54
|
+
if (entry.startsWith("*.") && !entry.includes("/")) return path.endsWith(entry.slice(1));
|
|
55
|
+
return overlaps([path], [entry.replace(/\/\*\*$/, "")]).length > 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
/**
|
|
37
59
|
* Spec §4.4: `changedOwned` are the changes inside the lane's Owns; `violations` the ones outside it
|
|
38
60
|
* that no overlapping dispatch owns either. `strict` is false for a laneless dispatch that may write
|
|
@@ -44,7 +66,7 @@ export function splitChanges(
|
|
|
44
66
|
others: string[],
|
|
45
67
|
strict: boolean,
|
|
46
68
|
): { changedOwned: string[]; violations: string[] } {
|
|
47
|
-
const inside = (p: string, paths: string[]) =>
|
|
69
|
+
const inside = (p: string, paths: string[]) => paths.some((entry) => ownsPath(p, entry));
|
|
48
70
|
return {
|
|
49
71
|
changedOwned: changed.filter((p) => inside(p, owns)),
|
|
50
72
|
violations: strict ? changed.filter((p) => !inside(p, owns) && !inside(p, others)) : [],
|
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
|
|
|
@@ -51,35 +49,27 @@ export const CHATGPT_UNIT_USD = 0.019;
|
|
|
51
49
|
export interface Cost {
|
|
52
50
|
/** 0: paid from a subscription; 1: metered. Every tier-0 rung ranks before any tier-1 rung. */
|
|
53
51
|
tier: 0 | 1;
|
|
54
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* comparable across billing modes, in list-price dollars per task; 0 for an unpriced rung paid from a plan or
|
|
54
|
+
* subscription (it spends nothing beyond the flat fee); null for an unpriced metered one
|
|
55
|
+
*/
|
|
55
56
|
value: number | null;
|
|
56
57
|
mode: BillingMode;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
* Spec §5.3. Every mode yields list-price dollars per task, so plans compare with each other
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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.
|
|
64
66
|
*/
|
|
65
|
-
export function costOf(family: Family | null, effort: string, mode: BillingMode): Cost {
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
tier: 0,
|
|
72
|
-
value:
|
|
73
|
-
family.planWeight === undefined ? usd : family.planWeight * CHATGPT_UNIT_USD * effortFactor(effort),
|
|
74
|
-
mode,
|
|
75
|
-
};
|
|
76
|
-
case "claude-plan":
|
|
77
|
-
return { tier: family.meteredOnPlan ? 1 : 0, value: usd, mode };
|
|
78
|
-
case "subscription":
|
|
79
|
-
return { tier: 0, value: usd, mode };
|
|
80
|
-
case "metered":
|
|
81
|
-
return { tier: 1, value: usd, mode };
|
|
82
|
-
}
|
|
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 };
|
|
83
73
|
}
|
|
84
74
|
|
|
85
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"
|
|
@@ -17,9 +16,13 @@ export type ErrorCode =
|
|
|
17
16
|
| "E_ADMIT_OVERLAP"
|
|
18
17
|
| "E_ADMIT_ID"
|
|
19
18
|
| "E_ADMIT_THREAD"
|
|
19
|
+
| "E_ADMIT_PAUSED"
|
|
20
|
+
| "E_ADMIT_ORDER"
|
|
20
21
|
| "E_LANE_INVALID"
|
|
21
22
|
| "E_LAND_GATE"
|
|
23
|
+
| "E_TREE"
|
|
22
24
|
| "E_CLIMB_DESIGN"
|
|
25
|
+
| "E_CLIMB_ENV"
|
|
23
26
|
| "E_JEV_KEY"
|
|
24
27
|
| "E_JEV_NETWORK"
|
|
25
28
|
| "E_JEV_RESPONSE"
|
|
@@ -28,10 +31,11 @@ export type ErrorCode =
|
|
|
28
31
|
| "E_IO_PATH"
|
|
29
32
|
| "E_IO_UNEXPECTED"
|
|
30
33
|
| "E_INPUT_INVALID"
|
|
34
|
+
| "E_ROLE_SCOPE"
|
|
31
35
|
| "E_RUNTIME_TOO_OLD";
|
|
32
36
|
|
|
33
37
|
/**
|
|
34
|
-
* 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
|
|
35
39
|
* nothing that is installed (audit N12).
|
|
36
40
|
*/
|
|
37
41
|
export const UPGRADE = "bun add -g catherd-cli@latest (or run bunx catherd-cli@latest <command>)";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The lines of a check's output that name a failure, stripped of colour, timings and counters so two runs compare
|
|
5
|
+
* equal: at most 60 lines of at most 300 characters (t3herd `failLines`).
|
|
6
|
+
*/
|
|
7
|
+
export function checkFailLines(out: string): string[] {
|
|
8
|
+
const seen = new Set<string>();
|
|
9
|
+
for (const raw of out.split("\n")) {
|
|
10
|
+
const line = raw
|
|
11
|
+
.replace(ANSI, "")
|
|
12
|
+
.replace(/\s*[[(]\s*\d+(\.\d+)?\s*(ms|s|m)\s*[\])]/g, "")
|
|
13
|
+
.replace(/\b\d+(\.\d+)?(ms|s)\b/g, "")
|
|
14
|
+
.trim();
|
|
15
|
+
const vuln = line.match(/^Vulnerability #\d+: (GO-\d{4}-\d+)/)?.[1];
|
|
16
|
+
if (vuln) seen.add(`vulnerability ${vuln}`);
|
|
17
|
+
else if (/(^|\s)(FAIL|FAILED|✗|✕|×)(\s|:|$)|^\(fail\)|^--- FAIL|^not ok\b|\bpanic:|^error:/.test(line))
|
|
18
|
+
seen.add(line.slice(0, 300));
|
|
19
|
+
if (seen.size >= 60) break;
|
|
20
|
+
}
|
|
21
|
+
return [...seen];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The lines of `fails` the base does not have; all of them when there is no base record. */
|
|
25
|
+
export const newFails = (fails: string[], base: string[] | null): string[] =>
|
|
26
|
+
fails.filter((f) => !base?.includes(f));
|