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.
Files changed (275) hide show
  1. package/README.md +164 -208
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -247
  5. package/package.json +6 -8
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/bin/catherd-mcp +1 -1
  9. package/plugin/commands/catherd.md +2 -2
  10. package/plugin/skills/catherd/SKILL.md +97 -421
  11. package/plugin/skills/catherd-setup/SKILL.md +20 -19
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +1 -2
  14. package/src/adapters/cursor/home.ts +1 -1
  15. package/src/adapters/cursor/index.ts +4 -4
  16. package/src/cli.ts +22 -27
  17. package/src/domain/bars.ts +33 -107
  18. package/src/domain/blocked.ts +72 -0
  19. package/src/domain/branch.ts +51 -0
  20. package/src/domain/brief.ts +21 -6
  21. package/src/domain/catalog.ts +69 -324
  22. package/src/domain/cost.ts +15 -30
  23. package/src/domain/errors.ts +2 -2
  24. package/src/domain/fail-lines.ts +26 -0
  25. package/src/domain/failover.ts +37 -87
  26. package/src/domain/findings.ts +47 -0
  27. package/src/domain/gate-brief.ts +1 -0
  28. package/src/domain/ids.ts +13 -2
  29. package/src/domain/jev.ts +47 -2
  30. package/src/domain/judge.ts +124 -0
  31. package/src/domain/lane.ts +22 -3
  32. package/src/domain/milestone-memory.ts +53 -0
  33. package/src/domain/notice.ts +25 -8
  34. package/src/domain/notify.ts +32 -0
  35. package/src/domain/plan-doc.ts +198 -0
  36. package/src/domain/plan-lint.ts +141 -0
  37. package/src/domain/profile-rules.ts +101 -160
  38. package/src/domain/profile.ts +116 -63
  39. package/src/domain/record.ts +53 -2
  40. package/src/domain/role-prompts.ts +131 -45
  41. package/src/domain/role-scope.ts +4 -34
  42. package/src/domain/role-tools.ts +2 -2
  43. package/src/domain/roles.ts +2 -0
  44. package/src/domain/route.ts +17 -0
  45. package/src/domain/rules.ts +33 -0
  46. package/src/domain/run-events.ts +298 -0
  47. package/src/domain/run-machine.ts +668 -0
  48. package/src/domain/run-state.ts +1310 -0
  49. package/src/domain/scorecard.ts +197 -0
  50. package/src/domain/select.ts +151 -146
  51. package/src/domain/slopalytics.ts +72 -0
  52. package/src/entry/answer-command.ts +39 -0
  53. package/src/entry/catalog-command.ts +25 -174
  54. package/src/entry/check-bin.ts +17 -0
  55. package/src/entry/cli-kit.ts +1 -1
  56. package/src/entry/daemon-bin.ts +22 -0
  57. package/src/entry/glyphs.ts +15 -2
  58. package/src/entry/init-command.ts +18 -68
  59. package/src/entry/knowledge-command.ts +45 -4
  60. package/src/entry/lock-command.ts +6 -7
  61. package/src/entry/mcp/role-server.ts +18 -0
  62. package/src/entry/mcp/server.ts +13 -40
  63. package/src/entry/mcp/session-tools.ts +149 -0
  64. package/src/entry/panel/openapi.ts +189 -0
  65. package/src/entry/panel/server.ts +410 -0
  66. package/src/entry/panel-command.ts +68 -0
  67. package/src/entry/profile-command.ts +10 -14
  68. package/src/entry/role-cli-command.ts +47 -3
  69. package/src/entry/runs-command.ts +3 -19
  70. package/src/entry/scorecard-command.ts +187 -0
  71. package/src/entry/supervise-bin.ts +1 -1
  72. package/src/infra/env.ts +0 -2
  73. package/src/infra/git.ts +64 -0
  74. package/src/infra/heavy-lock.ts +45 -7
  75. package/src/infra/launch.ts +63 -7
  76. package/src/infra/memory.ts +45 -0
  77. package/src/infra/ntfy.ts +40 -0
  78. package/src/infra/open-url.ts +42 -0
  79. package/src/infra/paths.ts +2 -2
  80. package/src/infra/slopalytics.ts +30 -0
  81. package/src/infra/store.ts +6 -1
  82. package/src/services/admission.ts +24 -9
  83. package/src/services/backends.ts +4 -6
  84. package/src/services/capture.ts +1 -22
  85. package/src/services/catalog-service.ts +155 -262
  86. package/src/services/credentials.ts +4 -20
  87. package/src/services/daemon.ts +181 -0
  88. package/src/services/dispatch-service.ts +5 -1
  89. package/src/services/doctor-backends.ts +1 -1
  90. package/src/services/doctor-checks.ts +47 -0
  91. package/src/services/doctor.ts +4 -9
  92. package/src/services/failover-rungs.ts +109 -0
  93. package/src/services/finalize.ts +12 -0
  94. package/src/services/gate-env.ts +1 -2
  95. package/src/services/gate-service.ts +21 -6
  96. package/src/services/jev-service.ts +2 -4
  97. package/src/services/lane-service.ts +34 -3
  98. package/src/services/milestone-check.ts +433 -0
  99. package/src/services/milestone-tree.ts +170 -0
  100. package/src/services/milestones.ts +35 -8
  101. package/src/services/notifier.ts +69 -2
  102. package/src/services/notify-service.ts +26 -0
  103. package/src/services/panel-setup.ts +116 -0
  104. package/src/services/plan-input.ts +151 -0
  105. package/src/services/ports.ts +30 -4
  106. package/src/services/preflight.ts +82 -8
  107. package/src/services/profile-service.ts +30 -3
  108. package/src/services/profile-store.ts +8 -3
  109. package/src/services/protocol.ts +4 -0
  110. package/src/services/provenance.ts +31 -90
  111. package/src/services/questions.ts +9 -0
  112. package/src/services/reconcile.ts +11 -0
  113. package/src/services/routing-service.ts +26 -15
  114. package/src/services/run-briefs.ts +477 -0
  115. package/src/services/run-client.ts +217 -0
  116. package/src/services/run-effects.ts +837 -0
  117. package/src/services/run-events-store.ts +174 -0
  118. package/src/services/run-evidence.ts +1 -1
  119. package/src/services/run-read.ts +221 -0
  120. package/src/services/run-report.ts +176 -0
  121. package/src/services/run-service.ts +55 -6
  122. package/src/services/run-store.ts +54 -7
  123. package/src/services/scorecard-metrics.ts +86 -0
  124. package/src/services/scorecard.ts +382 -0
  125. package/src/services/summary.ts +27 -3
  126. package/src/services/tree-path.ts +27 -0
  127. package/src/services/workspace-admission.ts +5 -13
  128. package/src/services/workspace-service.ts +5 -0
  129. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  130. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  131. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  132. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  133. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  134. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  135. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  136. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  137. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  138. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  139. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  140. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  141. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  142. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  143. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  144. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  145. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  146. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  147. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  148. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  149. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  150. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  151. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  152. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  153. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  154. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  155. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  156. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  157. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  158. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  159. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  160. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  161. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  162. package/web/dist/assets/index-C5IbylvM.css +2 -0
  163. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  164. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  165. package/web/dist/assets/label-DFSYBc62.js +1 -0
  166. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  167. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  168. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  169. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  170. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  171. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  172. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  173. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  174. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  175. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  176. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  177. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  178. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  179. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  180. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  181. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  182. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  183. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  184. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  185. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  186. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  187. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  188. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  189. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  190. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  191. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  192. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  193. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  194. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  195. package/web/dist/assets/time--9kSQbbp.js +1 -0
  196. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  197. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  198. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  199. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  200. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  201. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  202. package/web/dist/index.html +15 -0
  203. package/catalog/ATTRIBUTION.md +0 -44
  204. package/catalog/scores.json +0 -3202
  205. package/catalog/sources.json +0 -97
  206. package/src/adapters/antigravity/events.ts +0 -131
  207. package/src/adapters/antigravity/home.ts +0 -46
  208. package/src/adapters/antigravity/index.ts +0 -335
  209. package/src/adapters/antigravity/models.ts +0 -37
  210. package/src/adapters/grok/events.ts +0 -112
  211. package/src/adapters/grok/home.ts +0 -187
  212. package/src/adapters/grok/index.ts +0 -358
  213. package/src/adapters/grok/models.ts +0 -48
  214. package/src/domain/calibration.ts +0 -132
  215. package/src/domain/sources.ts +0 -144
  216. package/src/entry/mcp/dispatch-tools.ts +0 -52
  217. package/src/entry/mcp/lane-tools.ts +0 -131
  218. package/src/entry/mcp/protocol-tools.ts +0 -63
  219. package/src/entry/mcp/push-tools.ts +0 -16
  220. package/src/entry/mcp/run-tools.ts +0 -164
  221. package/src/entry/mcp/setup-tools.ts +0 -109
  222. package/src/entry/mcp/workspace-tools.ts +0 -142
  223. package/src/entry/tui/commands.ts +0 -563
  224. package/src/entry/tui/effects.ts +0 -385
  225. package/src/entry/tui/fixtures.ts +0 -637
  226. package/src/entry/tui/profile-edits.ts +0 -146
  227. package/src/entry/tui/profile-tree.ts +0 -429
  228. package/src/entry/tui/providers/app.tsx +0 -162
  229. package/src/entry/tui/providers/boundary.tsx +0 -57
  230. package/src/entry/tui/providers/data.tsx +0 -160
  231. package/src/entry/tui/providers/keymap.tsx +0 -166
  232. package/src/entry/tui/providers/root.tsx +0 -45
  233. package/src/entry/tui/providers/theme.tsx +0 -17
  234. package/src/entry/tui/providers/toast.tsx +0 -77
  235. package/src/entry/tui/run.tsx +0 -114
  236. package/src/entry/tui/state.ts +0 -400
  237. package/src/entry/tui/stories.ts +0 -114
  238. package/src/entry/tui/text.ts +0 -130
  239. package/src/entry/tui/theme.ts +0 -140
  240. package/src/entry/tui/views/app.tsx +0 -208
  241. package/src/entry/tui/views/dialogs.tsx +0 -41
  242. package/src/entry/tui/views/profile-actions.ts +0 -331
  243. package/src/entry/tui/views/profiles.tsx +0 -520
  244. package/src/entry/tui/views/runs.tsx +0 -619
  245. package/src/entry/tui/views/save-dialog.tsx +0 -344
  246. package/src/entry/tui/views/status.tsx +0 -213
  247. package/src/entry/tui/widgets/chrome.tsx +0 -114
  248. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  249. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  250. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  251. package/src/entry/tui/widgets/dialog.tsx +0 -68
  252. package/src/entry/tui/widgets/line.tsx +0 -92
  253. package/src/entry/tui/widgets/list.tsx +0 -197
  254. package/src/entry/tui/widgets/spinner.tsx +0 -49
  255. package/src/entry/tui/widgets/toast.tsx +0 -62
  256. package/src/infra/sources/arena.ts +0 -95
  257. package/src/infra/sources/artificial-analysis.ts +0 -119
  258. package/src/infra/sources/cache.ts +0 -126
  259. package/src/infra/sources/csv.ts +0 -38
  260. package/src/infra/sources/epoch.ts +0 -82
  261. package/src/infra/sources/http.ts +0 -75
  262. package/src/infra/sources/litellm.ts +0 -58
  263. package/src/infra/sources/models-dev.ts +0 -59
  264. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  265. package/src/infra/sources/openrouter-models.ts +0 -60
  266. package/src/infra/sources/rows.ts +0 -38
  267. package/src/infra/sources/vectara.ts +0 -69
  268. package/src/infra/sources/zip.ts +0 -48
  269. package/src/services/catalog-refresh.ts +0 -201
  270. package/src/services/doctor-sources.ts +0 -76
  271. package/src/services/runs-page.ts +0 -290
  272. package/src/services/source-derive.ts +0 -397
  273. package/src/services/source-sync.ts +0 -312
  274. package/src/services/standins.ts +0 -238
  275. 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
- type Catalog,
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
- downgradeDims,
16
- ladderDropDims,
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, KINDS } from "./lane.ts";
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 kinds and difficulties no worker rung clears (spec 1.5 plan 24), which only an
37
- * explicit `profile validate` asks for.
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 TREAT_LIKE_FIX = (rung: string) =>
56
- `map it with: catherd catalog treat-like ${rung} <a scored rung>; catalog_query lists them`;
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, and (spec 1.2 §6.1) an unscored rung or stand-in, never an error: a rung that leans on an inferred
164
- * stand-in is a "stand-in to confirm", and one with no value and no stand-in is skipped by routing. With the
165
- * stored `doc` `p` came from, a value this catherd does not know is also a warning, one that says how the
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.efforts.length > 0 && !effortOffered(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.efforts.length === 0 && r.effort !== "default")
240
- warnings.push({
187
+ else if (!info.pair) {
188
+ unindexed++;
189
+ (o.unindexed === "warning" ? warnings : errors).push({
241
190
  path: `${at}.rungs`,
242
- message: `${rung}: catherd cannot check its effort until ${r.backend} lists its models`,
243
- fix: "catherd catalog refresh",
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
- const down = prev === undefined ? [] : ladderDropDims(c, prev, rung);
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}: it scores below ${prev} on ${down.join(", ")}`,
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
- errors.push({
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 a scored rung its backend offers, or turn the role off with: catherd profile set ${at}.enabled false`,
230
+ fix: `enable an indexed rung its backend offers, or turn the role off with: catherd profile set ${at}.enabled false`,
279
231
  });
280
- if (usable.length > 1)
281
- // Claude rungs have no honesty score, so they clear no shipped bar: select() starts on one only as the
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("; ")}: those lanes start at the default rung and climb only onto rungs at least as strong (route names the closest)`,
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 = catalogRungs(c).filter((x) => {
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 unscored = !scoresOf(c, rungInfo(c, to).canonical);
354
- if (unscored)
355
- warnings.push({
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: `stand-in ${to} is unscored and no rung is near enough to stand in for it`,
358
- fix: TREAT_LIKE_FIX(to),
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
- const ranked = rankStandIns(c, p.billing, from, pool);
367
- // an unscored stand-in has its own warning above: no "downgrade" on every bar dim besides
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}, scoring below it on ${down.join(", ")}`,
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
- /** The role's default rung when it is unscored: what routing starts on instead. */
396
- function defaultFallback(c: Catalog, p: Profile, role: Role): string {
397
- try {
398
- return `routing falls back to ${defaultLadder(c, routingProfileOf(p, role), role).rung}`;
399
- } catch {
400
- return "routing has no rung to fall back to";
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
- /** `repo_code logic, hard` per kind: the kinds and difficulties no rung of `usable` clears. */
405
- function unreachable(c: Catalog, usable: Candidate[]): string[] {
406
- return KINDS.flatMap((k) => {
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 k of KINDS) for (const d of DIFFICULTIES) starts.add(select(c, rp, role, k, d).rung);
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