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,58 +0,0 @@
1
- import { z } from "zod";
2
- import { type SourceTransport, sourceGet } from "./http.ts";
3
- import { perMillion } from "./openrouter-models.ts";
4
- import { num } from "./rows.ts";
5
-
6
- /** Spec 1.2 §3.1: price and effort support, a cross-check only (MIT). */
7
- export const LITELLM_URL =
8
- "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
9
-
10
- export const fetchLiteLlm = async (t: SourceTransport = {}): Promise<unknown> =>
11
- (await sourceGet(LITELLM_URL, t)).json();
12
-
13
- /** The efforts LiteLLM flags per model (`supports_<effort>_reasoning_effort`). */
14
- export const LITELLM_EFFORTS = ["none", "minimal", "low", "xhigh", "max"] as const;
15
-
16
- export interface LiteLlmModel {
17
- id: string;
18
- provider: string;
19
- /** dollars per million tokens */
20
- price: { input: number; output: number; cached: number | null } | null;
21
- context: number | null;
22
- /** effort → whether LiteLLM says the model supports it; absent when it does not say */
23
- efforts: Partial<Record<(typeof LITELLM_EFFORTS)[number], boolean>>;
24
- }
25
-
26
- /** Spec 1.2 §3.5: the vendors' own APIs, whose prices the catalog's are. */
27
- const FIRST_PARTY = new Set(["openai", "anthropic"]);
28
-
29
- const EntrySchema = z.looseObject({ litellm_provider: z.string() });
30
-
31
- /** The first-party entries of LiteLLM's price file: an OpenAI or Anthropic provider, a key with no `/`. */
32
- export function parseLiteLlm(raw: unknown): LiteLlmModel[] {
33
- if (!raw || typeof raw !== "object") return [];
34
- const out: LiteLlmModel[] = [];
35
- for (const [id, v] of Object.entries(raw as Record<string, unknown>)) {
36
- const e = EntrySchema.safeParse(v);
37
- if (!e.success || !FIRST_PARTY.has(e.data.litellm_provider) || id.includes("/")) continue;
38
- const entry = e.data as Record<string, unknown>;
39
- const input = perMillion(entry.input_cost_per_token);
40
- const output = perMillion(entry.output_cost_per_token);
41
- const efforts: LiteLlmModel["efforts"] = {};
42
- for (const effort of LITELLM_EFFORTS) {
43
- const flag = entry[`supports_${effort}_reasoning_effort`];
44
- if (typeof flag === "boolean") efforts[effort] = flag;
45
- }
46
- out.push({
47
- id,
48
- provider: e.data.litellm_provider,
49
- price:
50
- input !== null && output !== null
51
- ? { input, output, cached: perMillion(entry.cache_read_input_token_cost) }
52
- : null,
53
- context: num(entry.max_input_tokens),
54
- efforts,
55
- });
56
- }
57
- return out;
58
- }
@@ -1,59 +0,0 @@
1
- import { z } from "zod";
2
- import { type SourceTransport, sourceGet } from "./http.ts";
3
- import { num } from "./rows.ts";
4
-
5
- /** Spec 1.2 §3.1: efforts, context, output limit, price, tool calling, modalities, release date (MIT). */
6
- export const MODELS_DEV_URL = "https://models.dev/api.json";
7
-
8
- export const fetchModelsDev = async (t: SourceTransport = {}): Promise<unknown> =>
9
- (await sourceGet(MODELS_DEV_URL, t)).json();
10
-
11
- /** What catherd reads of one models.dev model; each field null when the source does not say. */
12
- export interface ModelFacts {
13
- efforts: string[] | null;
14
- context: number | null;
15
- output: number | null;
16
- /** dollars per million tokens */
17
- price: { input: number; cached: number; output: number } | null;
18
- toolUse: boolean | null;
19
- imageIn: boolean | null;
20
- reasoning: boolean | null;
21
- releaseDate: string | null;
22
- }
23
-
24
- const ModelSchema = z.looseObject({
25
- reasoning: z.boolean().optional(),
26
- reasoning_options: z
27
- .array(z.looseObject({ type: z.string(), values: z.array(z.string()).optional() }))
28
- .optional(),
29
- tool_call: z.boolean().optional(),
30
- release_date: z.string().optional(),
31
- modalities: z.looseObject({ input: z.array(z.string()).optional() }).optional(),
32
- limit: z.looseObject({ context: z.unknown(), output: z.unknown() }).optional(),
33
- cost: z.looseObject({ input: z.unknown(), output: z.unknown(), cache_read: z.unknown() }).optional(),
34
- });
35
-
36
- /**
37
- * Spec 1.2 §3.5: `provider`'s model `id` in models.dev's api.json (a provider id such as `openai` or
38
- * `opencode-go`, and the model id within it); null when absent or unreadable.
39
- */
40
- export function modelsDevFacts(raw: unknown, provider: string, id: string): ModelFacts | null {
41
- const p = (raw as Record<string, { models?: Record<string, unknown> }> | null)?.[provider];
42
- const parsed = ModelSchema.safeParse(p?.models?.[id]);
43
- if (!parsed.success) return null;
44
- const m = parsed.data;
45
- const efforts = m.reasoning_options?.find((o) => o.type === "effort")?.values ?? null;
46
- const input = num(m.cost?.input);
47
- const output = num(m.cost?.output);
48
- return {
49
- efforts,
50
- context: num(m.limit?.context),
51
- output: num(m.limit?.output),
52
- price:
53
- input !== null && output !== null ? { input, cached: num(m.cost?.cache_read) ?? input, output } : null,
54
- toolUse: m.tool_call ?? null,
55
- imageIn: m.modalities?.input ? m.modalities.input.includes("image") : null,
56
- reasoning: m.reasoning ?? null,
57
- releaseDate: m.release_date ?? null,
58
- };
59
- }
@@ -1,52 +0,0 @@
1
- import { median } from "../../domain/util.ts";
2
- import { SourceError, type SourceTransport, sourceGet } from "./http.ts";
3
- import { isoDay, num, type SourceRow } from "./rows.ts";
4
-
5
- /** Spec 1.2 §3.1: per provider, uptime, latency and throughput over the last 30 minutes. */
6
- export const openRouterEndpointsUrl = (id: string): string =>
7
- `https://openrouter.ai/api/v1/models/${id}/endpoints`;
8
-
9
- /**
10
- * Plan 13 R-H: one request per OpenRouter id (one per catalog family OpenRouter lists), in parallel. An id
11
- * OpenRouter no longer knows (404) is left out; any other failure fails the source, which keeps its last answer.
12
- */
13
- export async function fetchOpenRouterEndpoints(
14
- ids: readonly string[],
15
- t: SourceTransport = {},
16
- ): Promise<Record<string, unknown>> {
17
- const got = await Promise.all(
18
- ids.map(async (id) => {
19
- try {
20
- return [id, (await sourceGet(openRouterEndpointsUrl(id), t)).json()] as const;
21
- } catch (e) {
22
- if (e instanceof SourceError && e.status === 404) return null;
23
- throw e instanceof SourceError ? new SourceError(`${id}: ${e.message}`, e.status, e.headers) : e;
24
- }
25
- }),
26
- );
27
- return Object.fromEntries(got.filter((x) => x !== null));
28
- }
29
-
30
- export const ENDPOINT_FIELDS = ["latency_last_30m", "throughput_last_30m", "uptime_last_30m"] as const;
31
-
32
- /** A statistic as a number, or as a percentile object (`{ p50: … }`) some answers carry. */
33
- const stat = (v: unknown): number | null =>
34
- v !== null && typeof v === "object" ? num((v as { p50?: unknown }).p50) : num(v);
35
-
36
- /** One row per OpenRouter id and field: the median over its endpoints that report the field. */
37
- export function parseOpenRouterEndpoints(raw: Record<string, unknown>, fetchedAt: string): SourceRow[] {
38
- const rows: SourceRow[] = [];
39
- for (const [id, answer] of Object.entries(raw)) {
40
- const endpoints = (answer as { data?: { endpoints?: unknown[] } } | null)?.data?.endpoints;
41
- if (!Array.isArray(endpoints)) continue;
42
- for (const field of ENDPOINT_FIELDS) {
43
- const xs = endpoints
44
- .map((e) => stat((e as Record<string, unknown> | null)?.[field]))
45
- .filter((x): x is number => x !== null);
46
- const value = median(xs);
47
- if (value !== null)
48
- rows.push({ rung: id, field, value, date: isoDay(fetchedAt), url: `https://openrouter.ai/${id}` });
49
- }
50
- }
51
- return rows;
52
- }
@@ -1,60 +0,0 @@
1
- import { z } from "zod";
2
- import { type SourceTransport, sourceGet } from "./http.ts";
3
- import { num } from "./rows.ts";
4
-
5
- /** Spec 1.2 §3.1: price and context cross-check; a model appears here the day it ships. */
6
- export const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
7
-
8
- export const fetchOpenRouterModels = async (t: SourceTransport = {}): Promise<unknown> =>
9
- (await sourceGet(OPENROUTER_MODELS_URL, t)).json();
10
-
11
- export interface OpenRouterModel {
12
- /** `author/slug`, as the endpoints path takes it */
13
- id: string;
14
- context: number | null;
15
- /** dollars per million tokens */
16
- price: { input: number; output: number; cached: number | null } | null;
17
- efforts: string[] | null;
18
- defaultEffort: string | null;
19
- }
20
-
21
- const ModelSchema = z.looseObject({
22
- id: z.string(),
23
- context_length: z.unknown(),
24
- pricing: z
25
- .looseObject({ prompt: z.unknown(), completion: z.unknown(), input_cache_read: z.unknown() })
26
- .optional(),
27
- reasoning: z
28
- .looseObject({ supported_efforts: z.array(z.string()).optional(), default_effort: z.string().optional() })
29
- .optional(),
30
- });
31
-
32
- /** A per-token price (a number or decimal string) per million tokens, without float noise (0.000002 × 1e6). */
33
- export const perMillion = (v: unknown): number | null => {
34
- const n = num(v);
35
- return n === null ? null : Math.round(n * 1e12) / 1e6;
36
- };
37
-
38
- /** Every model of OpenRouter's `/models` answer; a malformed entry is skipped. */
39
- export function parseOpenRouterModels(raw: unknown): OpenRouterModel[] {
40
- const data = (raw as { data?: unknown[] } | null)?.data;
41
- if (!Array.isArray(data)) return [];
42
- const out: OpenRouterModel[] = [];
43
- for (const entry of data) {
44
- const m = ModelSchema.safeParse(entry);
45
- if (!m.success) continue;
46
- const input = perMillion(m.data.pricing?.prompt);
47
- const output = perMillion(m.data.pricing?.completion);
48
- out.push({
49
- id: m.data.id,
50
- context: num(m.data.context_length),
51
- price:
52
- input !== null && output !== null
53
- ? { input, output, cached: perMillion(m.data.pricing?.input_cache_read) }
54
- : null,
55
- efforts: m.data.reasoning?.supported_efforts ?? null,
56
- defaultEffort: m.data.reasoning?.default_effort ?? null,
57
- });
58
- }
59
- return out;
60
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * Spec 1.2 §3.3: what a score source's parser returns, one number each. `rung` is the source's own model id,
3
- * with `#<effort>` when the source names one (catherd's effort word); `date` is the day the source measured or
4
- * published it, else the day it was fetched; `url` is where the value can be checked.
5
- */
6
- export interface SourceRow {
7
- rung: string;
8
- field: string;
9
- value: number;
10
- date: string;
11
- url: string;
12
- }
13
-
14
- /** The `YYYY-MM-DD` of an ISO time. */
15
- export const isoDay = (iso: string): string => iso.slice(0, 10);
16
-
17
- /** `candidate` when it is an http(s) URL, else `fallback`. */
18
- export const urlOr = (candidate: unknown, fallback: string): string =>
19
- typeof candidate === "string" && /^https?:\/\//.test(candidate.trim()) && URL.canParse(candidate.trim())
20
- ? candidate.trim()
21
- : fallback;
22
-
23
- /** `v` as a finite number (numbers and numeric strings), else null. */
24
- export function num(v: unknown): number | null {
25
- const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : Number.NaN;
26
- return Number.isFinite(n) ? n : null;
27
- }
28
-
29
- /** One row per rung and field: a source that lists a model twice (Epoch: one row per agent) keeps the highest. */
30
- export function keepHighest(rows: SourceRow[]): SourceRow[] {
31
- const best = new Map<string, SourceRow>();
32
- for (const r of rows) {
33
- const k = `${r.rung}\u0000${r.field}`;
34
- const had = best.get(k);
35
- if (!had || r.value > had.value) best.set(k, r);
36
- }
37
- return [...best.values()];
38
- }
@@ -1,69 +0,0 @@
1
- import { type SourceTransport, sourceGet } from "./http.ts";
2
- import { isoDay, num, type SourceRow } from "./rows.ts";
3
-
4
- /** Spec 1.2 §3.1: the leaderboard is a markdown table in the repository's README (Apache-2.0). */
5
- export const VECTARA_URL =
6
- "https://raw.githubusercontent.com/vectara/hallucination-leaderboard/main/README.md";
7
- export const VECTARA_PAGE = "https://github.com/vectara/hallucination-leaderboard";
8
-
9
- export const fetchVectara = async (t: SourceTransport = {}): Promise<string> =>
10
- (await sourceGet(VECTARA_URL, t)).text();
11
-
12
- const MONTHS = [
13
- "january",
14
- "february",
15
- "march",
16
- "april",
17
- "may",
18
- "june",
19
- "july",
20
- "august",
21
- "september",
22
- "october",
23
- "november",
24
- "december",
25
- ];
26
-
27
- /** "Last updated on September 22, 2026" as `2026-09-22`; null when the README has no such line. */
28
- export function vectaraDate(markdown: string): string | null {
29
- const m = /Last updated on ([A-Za-z]+) (\d{1,2}), (\d{4})/.exec(markdown);
30
- const month = m ? MONTHS.indexOf((m[1] as string).toLowerCase()) : -1;
31
- if (!m || month < 0) return null;
32
- return `${m[3]}-${String(month + 1).padStart(2, "0")}-${(m[2] as string).padStart(2, "0")}`;
33
- }
34
-
35
- const cells = (line: string) =>
36
- line
37
- .trim()
38
- .replace(/^\||\|$/g, "")
39
- .split("|")
40
- .map((c) => c.trim());
41
-
42
- /**
43
- * Two rows per model of the README's table: `factual_consistency` and `hallucination_rate`, in percent. The
44
- * model keeps its `vendor/` prefix and names no effort.
45
- */
46
- export function parseVectara(markdown: string, fetchedAt: string): SourceRow[] {
47
- const lines = markdown.split("\n");
48
- const head = lines.findIndex((l) => /^\|\s*Model\s*\|/i.test(l));
49
- if (head < 0) return [];
50
- const columns = cells(lines[head] as string).map((c) => c.toLowerCase());
51
- const at = (name: string) => columns.findIndex((c) => c.startsWith(name));
52
- const [consistency, hallucination] = [at("factual consistency"), at("hallucination rate")];
53
- const date = vectaraDate(markdown) ?? isoDay(fetchedAt);
54
- const rows: SourceRow[] = [];
55
- for (const line of lines.slice(head + 2)) {
56
- if (!line.trim().startsWith("|")) break;
57
- const c = cells(line);
58
- const model = c[0];
59
- if (!model) continue;
60
- for (const [field, i] of [
61
- ["factual_consistency", consistency],
62
- ["hallucination_rate", hallucination],
63
- ] as const) {
64
- const value = i < 0 ? null : num((c[i] ?? "").replace("%", ""));
65
- if (value !== null) rows.push({ rung: model, field, value, date, url: VECTARA_PAGE });
66
- }
67
- }
68
- return rows;
69
- }
@@ -1,48 +0,0 @@
1
- // Plan 13 R-F: Epoch ships its tables as one zip. Reading it needs only the central directory and
2
- // raw deflate (`Bun.inflateSync`), so there is no dependency for it.
3
-
4
- const EOCD = 0x06054b50;
5
- const CENTRAL = 0x02014b50;
6
- const LOCAL = 0x04034b50;
7
-
8
- /**
9
- * The files of a zip archive whose names `want` accepts, by name: stored (method 0) or deflated (8). Throws
10
- * on a malformed archive or another method. ZIP64 archives are not read (Epoch's is 2 MB).
11
- */
12
- export function unzip(
13
- bytes: Uint8Array,
14
- want: (name: string) => boolean = () => true,
15
- ): Map<string, Uint8Array> {
16
- const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
17
- let end = -1;
18
- // the end record is 22 bytes plus a comment of up to 64 KiB
19
- for (let i = bytes.length - 22; i >= Math.max(0, bytes.length - 22 - 0xffff); i--)
20
- if (dv.getUint32(i, true) === EOCD) {
21
- end = i;
22
- break;
23
- }
24
- if (end < 0) throw new Error("not a zip archive");
25
- const count = dv.getUint16(end + 10, true);
26
- let at = dv.getUint32(end + 16, true);
27
- const decode = new TextDecoder();
28
- const out = new Map<string, Uint8Array>();
29
- for (let k = 0; k < count; k++) {
30
- if (dv.getUint32(at, true) !== CENTRAL) throw new Error("a malformed zip directory");
31
- const method = dv.getUint16(at + 10, true);
32
- const size = dv.getUint32(at + 20, true);
33
- const nameLen = dv.getUint16(at + 28, true);
34
- const extraLen = dv.getUint16(at + 30, true);
35
- const commentLen = dv.getUint16(at + 32, true);
36
- const local = dv.getUint32(at + 42, true);
37
- const name = decode.decode(bytes.subarray(at + 46, at + 46 + nameLen));
38
- at += 46 + nameLen + extraLen + commentLen;
39
- if (!want(name)) continue;
40
- if (dv.getUint32(local, true) !== LOCAL) throw new Error(`a malformed zip entry: ${name}`);
41
- const start = local + 30 + dv.getUint16(local + 26, true) + dv.getUint16(local + 28, true);
42
- const data = bytes.subarray(start, start + size);
43
- if (method === 0) out.set(name, data);
44
- else if (method === 8) out.set(name, Bun.inflateSync(data.slice()));
45
- else throw new Error(`zip method ${method} is not read: ${name}`);
46
- }
47
- return out;
48
- }
@@ -1,201 +0,0 @@
1
- import { writeFileSync } from "node:fs";
2
- import { isDeepStrictEqual } from "node:util";
3
- import { barPool, deriveBars } from "../domain/bars.ts";
4
- import {
5
- buildCatalog,
6
- DIMS,
7
- type Dim,
8
- type ModelsFile,
9
- outranks,
10
- type Score,
11
- type ScoresFile,
12
- scoresOf,
13
- } from "../domain/catalog.ts";
14
- import { DIFFICULTIES, type Difficulty, KINDS, type Kind } from "../domain/lane.ts";
15
- import { defaultEffortOf, type SourcesFile } from "../domain/sources.ts";
16
- import type { SourceTransport } from "../infra/sources/http.ts";
17
- import { adjacent, derive, type RawAnswers } from "./source-derive.ts";
18
- import { cachedAnswers, syncSources } from "./source-sync.ts";
19
-
20
- /** Spec 1.2 §7: a value that moved by more than this is listed in the refresh PR. */
21
- const MOVED = 0.05;
22
-
23
- export interface RefreshReport {
24
- /** the day the data was read, `scores.json`'s new `version` */
25
- date: string;
26
- /** canonical rungs with no value in the shipped file before, and at least one now */
27
- newlyScored: string[];
28
- /** values that moved by more than 5 %, and values that appeared or went away on a rung already scored */
29
- moved: { rung: string; dim: Dim; from: number | null; to: number | null }[];
30
- /** every default threshold that moved, appeared or went away */
31
- barsMoved: { kind: Kind; difficulty: Difficulty; dim: Dim; from: number | null; to: number | null }[];
32
- /** shipped treat-likes dropped because their rung now has a value of its own on every dimension they lent */
33
- standInsDropped: string[];
34
- }
35
-
36
- const key = (s: Pick<Score, "rung" | "dim">) => `${s.rung} ${s.dim}`;
37
-
38
- /** One value per rung and dimension, the one that outranks the others (spec 1.2 §4.3), in file order. */
39
- function best(scores: Score[], now: number): Score[] {
40
- const out = new Map<string, Score>();
41
- for (const s of scores) {
42
- const had = out.get(key(s));
43
- if (!had || outranks(s, had, now)) out.set(key(s), s);
44
- }
45
- return [...out.values()].sort(
46
- (a, b) => a.rung.localeCompare(b.rung) || DIMS.indexOf(a.dim) - DIMS.indexOf(b.dim),
47
- );
48
- }
49
-
50
- /**
51
- * Spec 1.2 §5.2, §7: `catalog/scores.json` rebuilt from the keyless sources' answers. It keeps every hand-typed
52
- * value (one with no `source`), replaces the keyless values with what `raw` says now (Artificial Analysis is
53
- * never shipped, whatever `raw` holds), spreads every published value to the family's other efforts as
54
- * `adjacent` (plan 14 Ruling 3), keeps one value per rung and dimension, derives the default bars with their
55
- * `barsWhy`, and drops a shipped treat-like its rung no longer needs.
56
- */
57
- export function rebuildShipped(
58
- raw: RawAnswers,
59
- ctx: { models: ModelsFile; scores: ScoresFile; sources: SourcesFile; now: number },
60
- ): { file: ScoresFile; report: RefreshReport } {
61
- const date = new Date(ctx.now).toISOString().slice(0, 10);
62
- const { "artificial-analysis": _aa, ...keyless } = raw;
63
- // hand-typed values, less the ones an earlier rebuild spread: those are spread again below
64
- const hand = ctx.scores.scores.filter((s) => s.source === undefined && s.confidence !== "adjacent");
65
- const synced = derive(keyless, { ...ctx, scores: { ...ctx.scores, scores: hand } }).scores.filter(
66
- (s) => s.source !== "artificial-analysis" && s.confidence !== "adjacent",
67
- );
68
- const direct = best([...hand, ...synced], ctx.now);
69
- // an inferred value is catherd's guess for its own rung only: it is never carried to another effort
70
- const spread = adjacent(
71
- ctx.models.families,
72
- direct.filter((s) => s.confidence !== "inferred"),
73
- new Set(),
74
- ctx.now,
75
- (f) => defaultEffortOf(ctx.sources, f),
76
- );
77
- const scores = best([...direct, ...spread], ctx.now);
78
- const benchmark = (d: Dim) => {
79
- const b = ctx.scores.benchmarks[d];
80
- return `${b.benchmark} ${b.version}`;
81
- };
82
- const { bars, barsWhy } = deriveBars(barPool(scores), benchmark, date);
83
-
84
- const draft: ScoresFile = { ...ctx.scores, version: date, scores, bars, barsWhy };
85
- const c = buildCatalog({ models: ctx.models, scores: { ...draft, treatLike: ctx.scores.treatLike } });
86
- const treatLike: ScoresFile["treatLike"] = {};
87
- const standInsDropped: string[] = [];
88
- for (const [rung, t] of Object.entries(ctx.scores.treatLike))
89
- if (scoresOf(c, rung)?.borrowed.length) treatLike[rung] = t;
90
- else standInsDropped.push(rung);
91
- const file: ScoresFile = { ...draft, treatLike };
92
- return { file, report: { date, ...diffShipped(ctx.scores, file), standInsDropped } };
93
- }
94
-
95
- const rungsOf = (f: ScoresFile) => new Set(f.scores.map((s) => s.rung));
96
-
97
- /** What moved between two shipped files (spec 1.2 §7's PR body). */
98
- function diffShipped(
99
- before: ScoresFile,
100
- after: ScoresFile,
101
- ): Pick<RefreshReport, "newlyScored" | "moved" | "barsMoved"> {
102
- const had = rungsOf(before);
103
- const newlyScored = [...rungsOf(after)].filter((r) => !had.has(r)).sort();
104
- const was = new Map(before.scores.map((s) => [key(s), s.value]));
105
- const now = new Map(after.scores.map((s) => [key(s), s.value]));
106
- const moved: RefreshReport["moved"] = [];
107
- for (const k of [...new Set([...was.keys(), ...now.keys()])].sort()) {
108
- const [rung, dim] = k.split(" ") as [string, Dim];
109
- if (newlyScored.includes(rung)) continue;
110
- const from = was.get(k) ?? null;
111
- const to = now.get(k) ?? null;
112
- const far =
113
- from === null || to === null || (from === 0 ? to !== 0 : Math.abs(to - from) / Math.abs(from) > MOVED);
114
- if (far) moved.push({ rung, dim, from, to });
115
- }
116
- const barsMoved: RefreshReport["barsMoved"] = [];
117
- for (const kind of KINDS)
118
- for (const difficulty of DIFFICULTIES)
119
- for (const dim of DIMS) {
120
- const from = before.bars[kind]?.[difficulty]?.[dim] ?? null;
121
- const to = after.bars[kind]?.[difficulty]?.[dim] ?? null;
122
- if (from !== to) barsMoved.push({ kind, difficulty, dim, from, to });
123
- }
124
- return { newlyScored, moved, barsMoved };
125
- }
126
-
127
- /** Whether the rebuilt file says anything new: its date and `barsWhy` wording alone never count. */
128
- export function shippedChanged(before: ScoresFile, after: ScoresFile): boolean {
129
- // as the file reads back: no undefined fields, whatever the key order
130
- const strip = (f: ScoresFile): unknown => JSON.parse(JSON.stringify({ ...f, version: "", barsWhy: null }));
131
- return !isDeepStrictEqual(strip(before), strip(after));
132
- }
133
-
134
- const num = (v: number | null) => (v === null ? "none" : String(v));
135
-
136
- /** Spec 1.2 §7: the refresh PR's body: rungs newly scored, values moved by more than 5 %, bars that moved. */
137
- export function refreshBody(r: RefreshReport): string {
138
- const out = [
139
- `Weekly refresh of the shipped scores from the keyless public sources (data of ${r.date}).`,
140
- "",
141
- "Artificial Analysis values are never shipped. Merging this releases a patch through the Release workflow.",
142
- "",
143
- `### Rungs newly scored (${r.newlyScored.length})`,
144
- "",
145
- ...(r.newlyScored.length ? r.newlyScored.map((x) => `- \`${x}\``) : ["none"]),
146
- "",
147
- `### Values moved by more than 5 % (${r.moved.length})`,
148
- "",
149
- ...(r.moved.length
150
- ? ["| rung | dimension | was | now |", "| --- | --- | --- | --- |"].concat(
151
- r.moved.map((m) => `| \`${m.rung}\` | ${m.dim} | ${num(m.from)} | ${num(m.to)} |`),
152
- )
153
- : ["none"]),
154
- "",
155
- `### Bars that moved (${r.barsMoved.length})`,
156
- "",
157
- ...(r.barsMoved.length
158
- ? ["| kind | difficulty | dimension | was | now |", "| --- | --- | --- | --- | --- |"].concat(
159
- r.barsMoved.map(
160
- (b) => `| ${b.kind} | ${b.difficulty} | ${b.dim} | ${num(b.from)} | ${num(b.to)} |`,
161
- ),
162
- )
163
- : ["none"]),
164
- ];
165
- if (r.standInsDropped.length)
166
- out.push(
167
- "",
168
- "### Shipped stand-ins no longer needed",
169
- "",
170
- ...r.standInsDropped.map((x) => `- \`${x}\` now has values of its own`),
171
- );
172
- return `${out.join("\n")}\n`;
173
- }
174
-
175
- /**
176
- * Spec 1.2 §7, `scripts/catalog-refresh.ts`: syncs every keyless source now (no Artificial Analysis key, the
177
- * TTL ignored) into the current data folder, rebuilds the shipped file from the answers and, when it says
178
- * anything new, writes it to `out`. A keyless source that fails writes nothing: a refresh never ships a file
179
- * that lost a source's values (plan 14 Ruling 11).
180
- */
181
- export async function refreshShipped(o: {
182
- out: string;
183
- current: ScoresFile;
184
- models: ModelsFile;
185
- sources: SourcesFile;
186
- transport?: SourceTransport;
187
- now?: () => number;
188
- }): Promise<{ changed: boolean; failed: { source: string; error: string }[]; report: RefreshReport | null }> {
189
- const now = o.now ?? Date.now;
190
- const sync = await syncSources({ force: true, aaKey: null, transport: o.transport, now });
191
- if (sync.failed.length) return { changed: false, failed: sync.failed, report: null };
192
- const { file, report } = rebuildShipped(cachedAnswers(), {
193
- models: o.models,
194
- scores: o.current,
195
- sources: o.sources,
196
- now: now(),
197
- });
198
- const changed = shippedChanged(o.current, file);
199
- if (changed) writeFileSync(o.out, `${JSON.stringify(file, null, 2)}\n`);
200
- return { changed, failed: [], report };
201
- }
@@ -1,76 +0,0 @@
1
- import type { Profile } from "../domain/profile.ts";
2
- import { type StandInToConfirm, standInsToConfirm } from "../domain/profile-rules.ts";
3
- import { loadCatalog } from "./catalog-service.ts";
4
- import type { Check } from "./doctor-checks.ts";
5
- import { runnableBackends } from "./profile-store.ts";
6
- import { sourcesStatus, TTL_MS } from "./source-sync.ts";
7
-
8
- const HOUR = 3_600_000;
9
- const DAY = 24 * HOUR;
10
-
11
- /** An age as doctor prints it: minutes under an hour, hours under two days, else days. */
12
- export function ageText(ms: number): string {
13
- if (ms < HOUR) return `${Math.max(1, Math.round(ms / 60_000))} min`;
14
- if (ms < 2 * DAY) return `${Math.round(ms / HOUR)} h`;
15
- return `${Math.round(ms / DAY)} d`;
16
- }
17
-
18
- /** Spec 1.2 §6.1, §9: the stand-ins to confirm across `profiles`, one per canonical rung. */
19
- export function standInsToConfirmIn(profiles: Profile[]): StandInToConfirm[] {
20
- if (profiles.length === 0) return [];
21
- const c = loadCatalog({ timings: false });
22
- const seen = new Map<string, StandInToConfirm>();
23
- for (const p of profiles)
24
- for (const x of standInsToConfirm(p, c, runnableBackends()))
25
- if (!seen.has(x.canonical)) seen.set(x.canonical, x);
26
- return [...seen.values()];
27
- }
28
-
29
- /** `gpt-5.6-terra#high (repo_code, terminal, honesty)`, the rung and the dimensions a stand-in fills. */
30
- const confirmText = (xs: StandInToConfirm[]): string =>
31
- `stand-ins to confirm: ${xs.map((x) => `${x.canonical} (${x.dims.map((d) => d.dim).join(", ")})`).join(", ")}`;
32
-
33
- /**
34
- * Spec 1.2 §9: the `sources` row: each source's age and last error, whether an Artificial Analysis key is
35
- * set, its requests left today (`x-ratelimit-remaining` of its last answer), and the linked profiles'
36
- * stand-ins to confirm (`confirm`, spec 1.2 §6.1). `info` before the first sync (routing uses the shipped
37
- * scores); a warning when a source fails or has not been fetched for two TTLs (a day), since background
38
- * syncs would have refreshed it. It reads the sync's state only: no request.
39
- */
40
- export function sourcesCheck(now = Date.now(), confirm: StandInToConfirm[] = []): Check {
41
- const s = sourcesStatus();
42
- const shown = s.sources.filter((x) => x.source !== "artificial-analysis" || s.aaKey);
43
- const keyless = s.sources.filter((x) => x.source !== "artificial-analysis");
44
- const sameDay =
45
- s.rateLimitAt !== null && s.rateLimitAt.slice(0, 10) === new Date(now).toISOString().slice(0, 10);
46
- const left =
47
- s.rateLimitRemaining === null
48
- ? ""
49
- : sameDay
50
- ? `, ${s.rateLimitRemaining} requests left today`
51
- : `, ${s.rateLimitRemaining} requests left on ${s.rateLimitAt?.slice(0, 10)}`;
52
- const aa = `${s.aaKey ? `Artificial Analysis key set${left}` : "no Artificial Analysis key"}${confirm.length ? `; ${confirmText(confirm)}` : ""}`;
53
- const base = { id: "sources", label: "sources" };
54
- if (keyless.every((x) => x.fetchedAt === null && x.error === null))
55
- return {
56
- ...base,
57
- state: "info",
58
- word: "not synced",
59
- detail: `routing uses the shipped scores; ${aa}`,
60
- fix: "catherd catalog sync",
61
- };
62
- const parts = shown.map(
63
- (x) =>
64
- `${x.source} ${x.fetchedAt ? ageText(now - Date.parse(x.fetchedAt)) : "never"}${x.error ? ` (${x.error})` : ""}`,
65
- );
66
- const failing = shown.some((x) => x.error !== null);
67
- const stale = keyless.some((x) => x.fetchedAt === null || now - Date.parse(x.fetchedAt) > 2 * TTL_MS);
68
- const warn = failing || stale;
69
- return {
70
- ...base,
71
- state: warn ? "warn" : "ok",
72
- word: failing ? "failing" : stale ? "stale" : "fresh",
73
- detail: `${parts.join(", ")}; ${aa}`,
74
- ...(warn ? { fix: "catherd catalog sync --force" } : {}),
75
- };
76
- }