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
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
import type { OrchestrationHost } from "./host.ts";
|
|
2
|
+
import { type Bars, barInversions, barOf, effectiveBars, percentilesOf } from "./bars.ts";
|
|
3
|
+
import { type Catalog, capableFor, effortOffered, ROLE_NEEDS, rungInfo } from "./catalog.ts";
|
|
2
4
|
import {
|
|
3
|
-
|
|
4
|
-
type Dim,
|
|
5
|
-
capableFor,
|
|
6
|
-
effortOffered,
|
|
7
|
-
ROLE_NEEDS,
|
|
8
|
-
type RungInfo,
|
|
9
|
-
rungInfo,
|
|
10
|
-
scoresOf,
|
|
11
|
-
} from "./catalog.ts";
|
|
12
|
-
import {
|
|
13
|
-
catalogRungs,
|
|
5
|
+
catalogPairs,
|
|
14
6
|
claudeBilled,
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
downgrade,
|
|
8
|
+
intelligenceOf,
|
|
9
|
+
ladderDrop,
|
|
17
10
|
quotaOf,
|
|
18
11
|
rankStandIns,
|
|
19
12
|
} from "./failover.ts";
|
|
20
|
-
import { tryParseRung } from "./ids.ts";
|
|
21
|
-
import { DIFFICULTIES,
|
|
13
|
+
import { effortRank, tryParseRung } from "./ids.ts";
|
|
14
|
+
import { DIFFICULTIES, type Difficulty } from "./lane.ts";
|
|
22
15
|
import { type Profile, type ProfileDoc, unknownValues } from "./profile.ts";
|
|
23
16
|
import { DEFAULT_ACCESS, ROLES, type Role } from "./roles.ts";
|
|
24
17
|
import {
|
|
25
18
|
type Candidate,
|
|
26
19
|
candidates,
|
|
20
|
+
CODING_FLOOR,
|
|
21
|
+
CODING_ROLES,
|
|
27
22
|
clearsBar,
|
|
28
23
|
defaultLadder,
|
|
29
24
|
type RoutingProfile,
|
|
@@ -33,11 +28,13 @@ import {
|
|
|
33
28
|
export { quotaOf };
|
|
34
29
|
|
|
35
30
|
/**
|
|
36
|
-
* `reach`: also warn about the
|
|
37
|
-
*
|
|
31
|
+
* `reach`: also warn about the difficulties no worker rung clears (spec 1.5 plan 24), which only an explicit
|
|
32
|
+
* `profile validate` asks for. `unindexed: "warning"` (doctor's) reads a rung with no Slopalytics index as a
|
|
33
|
+
* warning, since a profile can lose a pair overnight; everywhere else it is an error.
|
|
38
34
|
*/
|
|
39
35
|
export interface ValidateOptions {
|
|
40
36
|
reach?: boolean;
|
|
37
|
+
unindexed?: "error" | "warning";
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
/** One finding of `validate`: where in the profile, what is wrong, and the action that fixes it. */
|
|
@@ -52,10 +49,8 @@ export interface Validation {
|
|
|
52
49
|
warnings: Issue[];
|
|
53
50
|
}
|
|
54
51
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const SUGGEST_FIX = (rung: string) =>
|
|
58
|
-
`confirm or replace it: catherd catalog treat-like --suggest ${rung}, then catherd catalog treat-like ${rung} <a rung>`;
|
|
52
|
+
const SYNC_FIX = "catherd catalog sync";
|
|
53
|
+
const unindexedMessage = (rung: string) => `${rung} has no Slopalytics intelligence index: routing skips it`;
|
|
59
54
|
|
|
60
55
|
/** Spec 1.2 §6.2: two findings are the same one when they name the same path with the same message. */
|
|
61
56
|
const sameIssue = (a: Issue, b: Issue): boolean => a.path === b.path && a.message === b.message;
|
|
@@ -72,98 +67,23 @@ export function repairs(before: Validation | null, after: Validation): boolean {
|
|
|
72
67
|
return removed && !added;
|
|
73
68
|
}
|
|
74
69
|
|
|
75
|
-
/** Spec 1.2 §6.1 "stand-in to confirm": a rung whose values on some bar dimensions are an inferred stand-in's. */
|
|
76
|
-
export interface StandInToConfirm {
|
|
77
|
-
canonical: string;
|
|
78
|
-
/** the profile's rungs that run it, and where the first one is */
|
|
79
|
-
rungs: string[];
|
|
80
|
-
path: string;
|
|
81
|
-
dims: { dim: Dim; like: string }[];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Spec 1.2 §6.1: the profile's rungs that lean on an inferred stand-in, one entry per canonical rung. Only
|
|
86
|
-
* where bars choose: rungs of a role with more than one usable rung, and failover stand-ins; a role with one
|
|
87
|
-
* rung runs it whatever the bars say (plan 14 Ruling 13).
|
|
88
|
-
*/
|
|
89
|
-
export function standInsToConfirm(p: Profile, c: Catalog, backends: readonly string[]): StandInToConfirm[] {
|
|
90
|
-
const seen = new Map<string, StandInToConfirm>();
|
|
91
|
-
const add = (rung: string, path: string) => {
|
|
92
|
-
const r = tryParseRung(rung);
|
|
93
|
-
if (!r || !backends.includes(r.backend)) return;
|
|
94
|
-
const info = rungInfo(c, rung);
|
|
95
|
-
const s = scoresOf(c, info.canonical);
|
|
96
|
-
if (!s || s.inferred.length === 0) return;
|
|
97
|
-
const had = seen.get(info.canonical);
|
|
98
|
-
if (had) {
|
|
99
|
-
if (!had.rungs.includes(rung)) had.rungs.push(rung);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const dims = s.inferred.map((dim) => ({ dim, like: s.standIns[dim] as string }));
|
|
103
|
-
seen.set(info.canonical, { canonical: info.canonical, rungs: [rung], path, dims });
|
|
104
|
-
};
|
|
105
|
-
for (const role of ROLES) {
|
|
106
|
-
const rc = p.roles[role];
|
|
107
|
-
if (!rc.enabled || candidates(c, routingProfileOf(p, role), role).length < 2) continue;
|
|
108
|
-
for (const rung of rc.rungs) add(rung, `roles.${role}.rungs`);
|
|
109
|
-
}
|
|
110
|
-
for (const [from, to] of Object.entries(p.failover)) add(to, `failover.${from}`);
|
|
111
|
-
return [...seen.values()];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** One "stand-in to confirm" warning's words. */
|
|
115
|
-
export function standInMessage(x: StandInToConfirm): string {
|
|
116
|
-
const words = (xs: string[]) =>
|
|
117
|
-
xs.length < 3 ? xs.join(" or ") : `${xs.slice(0, -1).join(", ")} or ${xs.at(-1)}`;
|
|
118
|
-
const lent = x.dims.map((d) => `${d.like}'s ${d.dim}`).join(", ");
|
|
119
|
-
const also = x.rungs.length > 1 ? ` (${x.rungs.join(", ")})` : "";
|
|
120
|
-
return `stand-in to confirm: ${x.canonical}${also} has no ${words(x.dims.map((d) => d.dim))} value of its own; routing uses ${lent} (inferred)`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
70
|
export const routingProfileOf = (p: Profile, role: Role): RoutingProfile => ({
|
|
124
71
|
objective: p.objective,
|
|
125
72
|
billing: p.billing,
|
|
126
73
|
role: p.roles[role],
|
|
74
|
+
maxEffort: p.maxEffort,
|
|
75
|
+
bars: { ...p.bars, ...p.roles[role].bars },
|
|
127
76
|
});
|
|
128
77
|
|
|
129
|
-
/**
|
|
130
|
-
* Whether a rung's scores are catherd's guess: borrowed through a treat-like, filled by an inferred stand-in
|
|
131
|
-
* (spec 1.2 §6.1), or only `inferred` ones. `note` says what a treat-like lends: "scores borrowed from X"
|
|
132
|
-
* when every value is borrowed, else the dimensions it borrows (a rung a sync scored on some); and (1.2
|
|
133
|
-
* minor) what each inferred stand-in lends: "honesty inferred from Y".
|
|
134
|
-
*/
|
|
135
|
-
export function inferredScores(
|
|
136
|
-
c: Catalog,
|
|
137
|
-
info: RungInfo,
|
|
138
|
-
): { inferred: boolean; via: string | null; note: string | null } {
|
|
139
|
-
const s = scoresOf(c, info.canonical);
|
|
140
|
-
if (!s) return { inferred: false, via: null, note: null };
|
|
141
|
-
const records = Object.values(s.records);
|
|
142
|
-
const guessed =
|
|
143
|
-
s.via !== null || s.inferred.length > 0 || records.every((r) => r.confidence === "inferred");
|
|
144
|
-
const all = s.borrowed.length === records.length;
|
|
145
|
-
const byStandIn = new Map<string, Dim[]>();
|
|
146
|
-
for (const d of s.inferred) {
|
|
147
|
-
const like = s.standIns[d] as string;
|
|
148
|
-
byStandIn.set(like, [...(byStandIn.get(like) ?? []), d]);
|
|
149
|
-
}
|
|
150
|
-
const parts = [
|
|
151
|
-
...(s.via ? [`${all ? "scores" : s.borrowed.join(", ")} borrowed from ${s.via}`] : []),
|
|
152
|
-
...[...byStandIn].map(([like, dims]) => `${dims.join(", ")} inferred from ${like}`),
|
|
153
|
-
];
|
|
154
|
-
return { inferred: guessed, via: s.via, note: parts.length ? parts.join("; ") : null };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
78
|
/**
|
|
158
79
|
* Spec §7.1. Errors: the worker disabled; an enabled role with no usable rung; a failover stand-in on the
|
|
159
80
|
* same quota; a rung whose backend catherd cannot run (`backends` lists those it can, `claude` included).
|
|
160
81
|
* Everything else worth knowing is a warning: an access mode other than the role's default, an effort or
|
|
161
82
|
* model the last listing does not offer, a stand-in that never runs, (spec 1.1 §11) a stand-in that
|
|
162
83
|
* downgrades its rung, one that spends Claude quota while another plan could stand in, and a ladder that goes
|
|
163
|
-
* down
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* value is read.
|
|
84
|
+
* down. A rung or stand-in with no Slopalytics index is an error (spec 2.0 decision 12; a warning with
|
|
85
|
+
* `o.unindexed: "warning"`): routing skips it. With the stored `doc` `p` came from, a value this catherd does
|
|
86
|
+
* not know is also a warning, one that says how the value is read.
|
|
167
87
|
*/
|
|
168
88
|
export function validateProfile(
|
|
169
89
|
p: Profile,
|
|
@@ -188,6 +108,21 @@ export function validateProfile(
|
|
|
188
108
|
fix: "catherd profile set roles.worker.enabled true",
|
|
189
109
|
});
|
|
190
110
|
|
|
111
|
+
// a harder difficulty's percentile below an easier one's would route a harder lane to a weaker rung
|
|
112
|
+
const barOrder = (at: string, bars: Bars): void => {
|
|
113
|
+
const eff = effectiveBars({ bars });
|
|
114
|
+
for (const [easier, harder] of barInversions(eff))
|
|
115
|
+
errors.push({
|
|
116
|
+
path: at,
|
|
117
|
+
message: `the ${harder} bar (${eff[harder]}) is below the ${easier} bar (${eff[easier]})`,
|
|
118
|
+
fix: `catherd profile set ${at}.${harder} ${eff[easier]}, or lower ${at}.${easier} to ${eff[harder]} or less`,
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
barOrder("bars", p.bars);
|
|
122
|
+
for (const role of ROLES)
|
|
123
|
+
if (Object.keys(p.roles[role].bars ?? {}).length > 0)
|
|
124
|
+
barOrder(`roles.${role}.bars`, { ...p.bars, ...p.roles[role].bars });
|
|
125
|
+
|
|
191
126
|
for (const role of ROLES) {
|
|
192
127
|
const rc = p.roles[role];
|
|
193
128
|
const at = `roles.${role}`;
|
|
@@ -204,6 +139,7 @@ export function validateProfile(
|
|
|
204
139
|
message: `network: false does nothing here: it applies to workspace-write roles, and ${role} runs ${rc.access}`,
|
|
205
140
|
fix: `catherd profile set ${at}.network null`,
|
|
206
141
|
});
|
|
142
|
+
let unindexed = 0;
|
|
207
143
|
for (const rung of rc.rungs) {
|
|
208
144
|
const native = nativeClaudeIssue(rung, host, `${at}.rungs`);
|
|
209
145
|
if (native) errors.push(native);
|
|
@@ -216,6 +152,18 @@ export function validateProfile(
|
|
|
216
152
|
});
|
|
217
153
|
continue;
|
|
218
154
|
}
|
|
155
|
+
const above = aboveCeiling(p.maxEffort, rung, `${at}.rungs`);
|
|
156
|
+
if (above) warnings.push(above);
|
|
157
|
+
if (
|
|
158
|
+
CODING_ROLES.includes(role) &&
|
|
159
|
+
effortRank(r.effort) >= 0 &&
|
|
160
|
+
effortRank(r.effort) < effortRank(CODING_FLOOR)
|
|
161
|
+
)
|
|
162
|
+
warnings.push({
|
|
163
|
+
path: `${at}.rungs`,
|
|
164
|
+
message: `${rung} is below ${CODING_FLOOR} effort: a coding worker never runs it`,
|
|
165
|
+
fix: `pick #${CODING_FLOOR} or above: catherd catalog list --role ${role}`,
|
|
166
|
+
});
|
|
219
167
|
if (!backends.includes(r.backend)) {
|
|
220
168
|
errors.push({
|
|
221
169
|
path: `${at}.rungs`,
|
|
@@ -231,36 +179,31 @@ export function validateProfile(
|
|
|
231
179
|
message: `${rung} cannot fill the ${role} role (it needs ${Object.keys(ROLE_NEEDS[role]).join(" + ")})`,
|
|
232
180
|
fix: `catalog_query({ role: "${role}" }) lists the models that can`,
|
|
233
181
|
});
|
|
234
|
-
if (info.
|
|
182
|
+
if (info.listed === true && !effortOffered(info))
|
|
235
183
|
errors.push({
|
|
236
184
|
path: `${at}.rungs`,
|
|
237
185
|
message: `${r.model} has no effort "${r.effort}" on ${r.backend} (it has ${info.efforts.join(", ")})`,
|
|
238
186
|
});
|
|
239
|
-
else if (info.
|
|
240
|
-
|
|
187
|
+
else if (!info.pair) {
|
|
188
|
+
unindexed++;
|
|
189
|
+
(o.unindexed === "warning" ? warnings : errors).push({
|
|
241
190
|
path: `${at}.rungs`,
|
|
242
|
-
message:
|
|
243
|
-
fix:
|
|
191
|
+
message: unindexedMessage(rung),
|
|
192
|
+
fix: c.indexedAt === null ? SYNC_FIX : `pick an indexed pair: catherd catalog list --role ${role}`,
|
|
244
193
|
});
|
|
194
|
+
}
|
|
245
195
|
if (info.listed === false)
|
|
246
196
|
warnings.push({
|
|
247
197
|
path: `${at}.rungs`,
|
|
248
198
|
message: `${r.backend}'s last listing does not offer ${r.model}; routing skips it`,
|
|
249
199
|
});
|
|
250
|
-
if (!scoresOf(c, info.canonical))
|
|
251
|
-
warnings.push({
|
|
252
|
-
path: `${at}.rungs`,
|
|
253
|
-
message: `${rung} is unscored and no rung is near enough to stand in for it: routing skips it${rung === rc.defaultRung ? `; it is the ${role}'s default rung, so ${defaultFallback(c, p, role)}` : ""}`,
|
|
254
|
-
fix: TREAT_LIKE_FIX(rung),
|
|
255
|
-
});
|
|
256
200
|
}
|
|
257
201
|
rc.rungs.forEach((rung, i) => {
|
|
258
202
|
const prev = rc.rungs[i - 1];
|
|
259
|
-
|
|
260
|
-
if (down.length)
|
|
203
|
+
if (prev !== undefined && ladderDrop(c, prev, rung))
|
|
261
204
|
warnings.push({
|
|
262
205
|
path: `${at}.rungs`,
|
|
263
|
-
message: `the ladder goes down at ${rung}:
|
|
206
|
+
message: `the ladder goes down at ${rung}: intelligence ${(intelligenceOf(c, rung) as number).toFixed(1)} below ${prev}'s ${(intelligenceOf(c, prev) as number).toFixed(1)}`,
|
|
264
207
|
fix: `order ${at}.rungs weakest first`,
|
|
265
208
|
});
|
|
266
209
|
});
|
|
@@ -272,33 +215,30 @@ export function validateProfile(
|
|
|
272
215
|
});
|
|
273
216
|
const usable = candidates(c, routingProfileOf(p, role), role);
|
|
274
217
|
if (usable.length === 0)
|
|
275
|
-
|
|
218
|
+
// doctor's mode: rungs that are only unindexed are the same warning, so the role's total is one too, but
|
|
219
|
+
// only when an index exists: with none (not fetched, corrupt, empty) nothing can route, which is an error
|
|
220
|
+
(o.unindexed === "warning" &&
|
|
221
|
+
c.indexedAt !== null &&
|
|
222
|
+
Object.keys(c.index).length > 0 &&
|
|
223
|
+
unindexed > 0 &&
|
|
224
|
+
unindexed === rc.rungs.length
|
|
225
|
+
? warnings
|
|
226
|
+
: errors
|
|
227
|
+
).push({
|
|
276
228
|
path: `${at}.rungs`,
|
|
277
229
|
message: `the ${role} role has no usable rung`,
|
|
278
|
-
fix: `enable
|
|
230
|
+
fix: `enable an indexed rung its backend offers, or turn the role off with: catherd profile set ${at}.enabled false`,
|
|
279
231
|
});
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// default rung (when nothing clears) and never climbs onto it
|
|
283
|
-
for (const x of usable)
|
|
284
|
-
if (
|
|
285
|
-
quotaOf(x.info.parsed) === "claude-code" &&
|
|
286
|
-
!KINDS.some((k) => DIFFICULTIES.some((d) => clearsBar(c, x, k, d)))
|
|
287
|
-
)
|
|
288
|
-
warnings.push({
|
|
289
|
-
path: `${at}.rungs`,
|
|
290
|
-
message: `${x.rung} clears no routing bar, so a lane starts on it only as the role's default rung and never climbs onto it`,
|
|
291
|
-
});
|
|
292
|
-
// spec 1.5 plan 24: every kind and difficulty no worker rung clears, in one warning; those lanes start at the
|
|
293
|
-
// default rung (or an easier difficulty's start) and climb only onto rungs at least as strong. Only an
|
|
232
|
+
// spec 1.5 plan 24: every difficulty no worker rung clears, in one warning; those lanes start at the
|
|
233
|
+
// default rung (or an easier difficulty's start) and climb only onto rungs at least as intelligent. Only an
|
|
294
234
|
// explicit validate says it (Ruling 3): the shipped bars put logic and hard above every Sol rung, the
|
|
295
235
|
// owner's call, so every save and doctor would repeat it
|
|
296
236
|
if (o.reach && role === "worker" && usable.length > 1) {
|
|
297
|
-
const unreached = unreachable(c, usable);
|
|
237
|
+
const unreached = unreachable(c, usable, percentilesOf(routingProfileOf(p, role)));
|
|
298
238
|
if (unreached.length)
|
|
299
239
|
warnings.push({
|
|
300
240
|
path: `${at}.rungs`,
|
|
301
|
-
message: `no worker rung clears ${unreached.join("
|
|
241
|
+
message: `no worker rung clears ${unreached.join(", ")}: those lanes start at the default rung and climb only onto rungs at least as intelligent (route names the closest)`,
|
|
302
242
|
});
|
|
303
243
|
}
|
|
304
244
|
// spec 1.5 plan 24: a quota whose rungs never start a lane sits idle but for climbs (the identity run's Go).
|
|
@@ -328,7 +268,7 @@ export function validateProfile(
|
|
|
328
268
|
return b === undefined ? [] : b === "claude" ? [b, "claude-code"] : [b];
|
|
329
269
|
}),
|
|
330
270
|
);
|
|
331
|
-
const pool =
|
|
271
|
+
const pool = catalogPairs(c).filter((x) => {
|
|
332
272
|
const r = tryParseRung(x);
|
|
333
273
|
return r !== null && backends.includes(r.backend) && named.has(r.backend);
|
|
334
274
|
});
|
|
@@ -346,16 +286,19 @@ export function validateProfile(
|
|
|
346
286
|
});
|
|
347
287
|
continue;
|
|
348
288
|
}
|
|
289
|
+
const above = aboveCeiling(p.maxEffort, to, at);
|
|
290
|
+
if (above) warnings.push(above);
|
|
349
291
|
if (!backends.includes(b.backend)) {
|
|
350
292
|
errors.push({ path: at, message: `stand-in ${to}: catherd cannot run ${b.backend} yet` });
|
|
351
293
|
continue;
|
|
352
294
|
}
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
295
|
+
const ranked = rankStandIns(c, p.billing, from, pool);
|
|
296
|
+
const unindexed = intelligenceOf(c, to) === null;
|
|
297
|
+
if (unindexed)
|
|
298
|
+
(o.unindexed === "warning" ? warnings : errors).push({
|
|
356
299
|
path: at,
|
|
357
|
-
message:
|
|
358
|
-
fix:
|
|
300
|
+
message: unindexedMessage(to),
|
|
301
|
+
fix: c.indexedAt === null ? SYNC_FIX : `catherd profile set failover.${from} ${ranked[0] ?? "null"}`,
|
|
359
302
|
});
|
|
360
303
|
if (quotaOf(a) === quotaOf(b))
|
|
361
304
|
errors.push({
|
|
@@ -363,13 +306,11 @@ export function validateProfile(
|
|
|
363
306
|
message: `stand-in ${to} draws on the same quota as ${from}, which is out when ${from} hits its limit`,
|
|
364
307
|
fix: "name a stand-in on another backend or plan",
|
|
365
308
|
});
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const down = unscored ? [] : downgradeDims(c, from, to);
|
|
369
|
-
if (down.length)
|
|
309
|
+
// an unindexed stand-in has its own finding above: no "downgrade" besides
|
|
310
|
+
if (!unindexed && downgrade(c, from, to))
|
|
370
311
|
warnings.push({
|
|
371
312
|
path: at,
|
|
372
|
-
message: `downgrade: ${to} stands in for ${from},
|
|
313
|
+
message: `downgrade: ${to} stands in for ${from} at intelligence ${(intelligenceOf(c, to) as number).toFixed(1)}, below its ${(intelligenceOf(c, from) as number).toFixed(1)}`,
|
|
373
314
|
fix: `catherd profile set failover.${from} ${ranked[0] ?? "null"}`,
|
|
374
315
|
});
|
|
375
316
|
const other = ranked.find((x) => !claudeBilled(x));
|
|
@@ -387,26 +328,26 @@ export function validateProfile(
|
|
|
387
328
|
if (!ladders.has(from))
|
|
388
329
|
warnings.push({ path: at, message: `${from} is on no enabled role's ladder, so this never runs` });
|
|
389
330
|
}
|
|
390
|
-
for (const x of standInsToConfirm(p, c, backends))
|
|
391
|
-
warnings.push({ path: x.path, message: standInMessage(x), fix: SUGGEST_FIX(x.rungs[0] as string) });
|
|
392
331
|
return { errors, warnings };
|
|
393
332
|
}
|
|
394
333
|
|
|
395
|
-
/**
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
334
|
+
/** Plan 33: a warning for a rung above its model's `maxEffort`, null for any other. */
|
|
335
|
+
function aboveCeiling(maxEffort: Readonly<Record<string, string>>, rung: string, path: string): Issue | null {
|
|
336
|
+
const r = tryParseRung(rung);
|
|
337
|
+
const key = r ? `${r.backend}:${r.model}` : "";
|
|
338
|
+
const cap = maxEffort[key];
|
|
339
|
+
if (!r || cap === undefined || effortRank(r.effort) <= effortRank(cap) || effortRank(r.effort) < 0)
|
|
340
|
+
return null;
|
|
341
|
+
return {
|
|
342
|
+
path,
|
|
343
|
+
message: `${rung} is above maxEffort ${cap} for ${key}: routing skips it`,
|
|
344
|
+
fix: `catherd profile set maxEffort.${key} ${r.effort}, or remove the rung`,
|
|
345
|
+
};
|
|
402
346
|
}
|
|
403
347
|
|
|
404
|
-
/**
|
|
405
|
-
function unreachable(c: Catalog, usable: Candidate[]): string[] {
|
|
406
|
-
return
|
|
407
|
-
const ds = DIFFICULTIES.filter((d) => !usable.some((x) => clearsBar(c, x, k, d)));
|
|
408
|
-
return ds.length ? [`${k} ${ds.join(", ")}`] : [];
|
|
409
|
-
});
|
|
348
|
+
/** The difficulties no rung of `usable` clears, as `logic, hard`. */
|
|
349
|
+
function unreachable(c: Catalog, usable: Candidate[], at: Record<Difficulty, number>): string[] {
|
|
350
|
+
return DIFFICULTIES.filter((d) => !usable.some((x) => clearsBar(x, barOf(c, d, at))));
|
|
410
351
|
}
|
|
411
352
|
|
|
412
353
|
/**
|
|
@@ -422,7 +363,7 @@ function idleQuotas(c: Catalog, p: Profile, role: Role, usable: Candidate[]): [s
|
|
|
422
363
|
const usage = Object.fromEntries(quotas.map((q) => [q, q === fresh ? 0 : 1]));
|
|
423
364
|
const rp = { ...routingProfileOf(p, role), usage };
|
|
424
365
|
starts.add(defaultLadder(c, rp, role).rung);
|
|
425
|
-
for (const
|
|
366
|
+
for (const d of DIFFICULTIES) starts.add(select(c, rp, role, d).rung);
|
|
426
367
|
}
|
|
427
368
|
const started = new Set(usable.filter((x) => starts.has(x.rung)).map((x) => quotaOf(x.info.parsed)));
|
|
428
369
|
return quotas
|