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,312 +0,0 @@
|
|
|
1
|
-
import { buildCatalog, type Catalog, DIMS } from "../domain/catalog.ts";
|
|
2
|
-
import { CatherdError, errorMessage } from "../domain/errors.ts";
|
|
3
|
-
import {
|
|
4
|
-
type Derived,
|
|
5
|
-
SOURCE_IDS,
|
|
6
|
-
type SourceId,
|
|
7
|
-
type SourcesFile,
|
|
8
|
-
SourcesFileSchema,
|
|
9
|
-
} from "../domain/sources.ts";
|
|
10
|
-
import { assetPath } from "../infra/assets.ts";
|
|
11
|
-
import { log } from "../infra/log.ts";
|
|
12
|
-
import { type AaAnswer, fetchArtificialAnalysis } from "../infra/sources/artificial-analysis.ts";
|
|
13
|
-
import { fetchArena } from "../infra/sources/arena.ts";
|
|
14
|
-
import {
|
|
15
|
-
readCached,
|
|
16
|
-
readDerived,
|
|
17
|
-
readSyncState,
|
|
18
|
-
tryLockSync,
|
|
19
|
-
writeCached,
|
|
20
|
-
writeDerived,
|
|
21
|
-
writeSyncState,
|
|
22
|
-
} from "../infra/sources/cache.ts";
|
|
23
|
-
import { fetchEpoch } from "../infra/sources/epoch.ts";
|
|
24
|
-
import { rateLimitRemaining, SourceError, type SourceTransport } from "../infra/sources/http.ts";
|
|
25
|
-
import { fetchLiteLlm } from "../infra/sources/litellm.ts";
|
|
26
|
-
import { fetchModelsDev } from "../infra/sources/models-dev.ts";
|
|
27
|
-
import { fetchOpenRouterEndpoints } from "../infra/sources/openrouter-endpoints.ts";
|
|
28
|
-
import { fetchOpenRouterModels } from "../infra/sources/openrouter-models.ts";
|
|
29
|
-
import { fetchVectara } from "../infra/sources/vectara.ts";
|
|
30
|
-
import { readVersioned } from "../infra/store.ts";
|
|
31
|
-
import { readOverride, shippedModels, shippedScores } from "./catalog-service.ts";
|
|
32
|
-
import { aaKey } from "./credentials.ts";
|
|
33
|
-
import { derive, openRouterIds, type RawAnswers } from "./source-derive.ts";
|
|
34
|
-
|
|
35
|
-
/** Spec 1.2 §3.2: one TTL for every source. */
|
|
36
|
-
export const TTL_MS = 12 * 3_600_000;
|
|
37
|
-
/** A source whose last attempt failed waits this long before a background sync tries it again. */
|
|
38
|
-
const BACKOFF_MS = 3_600_000;
|
|
39
|
-
/** How long a foreground sync waits for one already running. */
|
|
40
|
-
const LOCK_WAIT_MS = 120_000;
|
|
41
|
-
|
|
42
|
-
let sourcesFile: SourcesFile | null = null;
|
|
43
|
-
/** `catalog/sources.json`: the sources, their licenses and attribution lines, the aliases. */
|
|
44
|
-
export const shippedSources = (): SourcesFile =>
|
|
45
|
-
(sourcesFile ??= readVersioned(assetPath("catalog/sources.json"), SourcesFileSchema, 1));
|
|
46
|
-
|
|
47
|
-
export interface SyncOptions {
|
|
48
|
-
/** ignore the TTL (and a failing source's backoff) */
|
|
49
|
-
force?: boolean;
|
|
50
|
-
/** the MCP server's boot sync: skips when another sync runs, and gives a failing source an hour's rest */
|
|
51
|
-
background?: boolean;
|
|
52
|
-
/** false: answer `busy` at once when another sync holds the lock (the catalog_sync tool), never wait */
|
|
53
|
-
wait?: boolean;
|
|
54
|
-
/** how every source is fetched; tests replace it */
|
|
55
|
-
transport?: SourceTransport;
|
|
56
|
-
/** the Artificial Analysis key; default: the env's, else the saved one */
|
|
57
|
-
aaKey?: string | null;
|
|
58
|
-
now?: () => number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface SourceOutcome {
|
|
62
|
-
source: SourceId;
|
|
63
|
-
/** fetched now; fresh (within the TTL); failed (the last good answer kept); skipped (no key, or backing off) */
|
|
64
|
-
state: "fetched" | "fresh" | "failed" | "skipped";
|
|
65
|
-
/** when the answer in the cache was fetched; null when there is none */
|
|
66
|
-
fetchedAt: string | null;
|
|
67
|
-
error?: string;
|
|
68
|
-
detail?: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface SyncReport {
|
|
72
|
-
/** another sync held the lock (a background sync never waits): nothing was done */
|
|
73
|
-
busy: boolean;
|
|
74
|
-
sources: SourceOutcome[];
|
|
75
|
-
/** canonical rungs with no value of their own before this sync and at least one after */
|
|
76
|
-
newlyScored: string[];
|
|
77
|
-
/** the user's treat-likes whose rung now has its own value on every dimension the stand-in lent */
|
|
78
|
-
noLongerNeeded: { rung: string; like: string }[];
|
|
79
|
-
failed: { source: SourceId; error: string }[];
|
|
80
|
-
/** the cross-checks that failed (spec 1.2 §3.5) */
|
|
81
|
-
warnings: string[];
|
|
82
|
-
/** each source's ids no family matched (spec 1.2 §3.4) */
|
|
83
|
-
unmatched: Record<string, string[]>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const iso = (ms: number) => new Date(ms).toISOString();
|
|
87
|
-
|
|
88
|
-
/** The catalog as routing would read it with `derived` as the synced layer. */
|
|
89
|
-
const catalogWith = (derived: Derived | null, now: number): Catalog =>
|
|
90
|
-
buildCatalog({
|
|
91
|
-
models: shippedModels(),
|
|
92
|
-
scores: shippedScores(),
|
|
93
|
-
synced: derived?.scores,
|
|
94
|
-
facts: derived?.facts,
|
|
95
|
-
override: readOverride(),
|
|
96
|
-
now,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const scored = (c: Catalog): Set<string> =>
|
|
100
|
-
new Set(Object.entries(c.scores).flatMap(([rung, dims]) => (Object.keys(dims).length ? [rung] : [])));
|
|
101
|
-
|
|
102
|
-
async function takeLock(background: boolean): Promise<(() => void) | null> {
|
|
103
|
-
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
104
|
-
for (;;) {
|
|
105
|
-
const release = tryLockSync();
|
|
106
|
-
if (release || background) return release;
|
|
107
|
-
if (Date.now() > deadline)
|
|
108
|
-
throw new CatherdError("E_IO_LOCK", "another catalog sync is still running", {
|
|
109
|
-
fix: "wait for it to finish, then run catherd catalog sync again",
|
|
110
|
-
});
|
|
111
|
-
await Bun.sleep(200);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Every cached answer, as `derive` reads them (and `scripts/catalog-refresh.ts` rebuilds the shipped file). */
|
|
116
|
-
export function cachedAnswers(): RawAnswers {
|
|
117
|
-
const raw: RawAnswers = {};
|
|
118
|
-
for (const id of SOURCE_IDS) {
|
|
119
|
-
const c = readCached(id);
|
|
120
|
-
if (c) raw[id] = { fetchedAt: c.fetchedAt, data: c.data };
|
|
121
|
-
}
|
|
122
|
-
return raw;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Spec 1.2 §3.2, §3.3: fetches every source whose cached answer is older than the TTL (every source with
|
|
127
|
-
* `force`), all in parallel, and writes each answer atomically with its fetch time. A source that fails keeps
|
|
128
|
-
* its last good answer; the sync records the error and goes on. Then it derives the synced values from every
|
|
129
|
-
* cached answer. One sync runs at a time: a background sync skips when another holds the lock, a foreground
|
|
130
|
-
* one waits for it.
|
|
131
|
-
*/
|
|
132
|
-
export async function syncSources(o: SyncOptions = {}): Promise<SyncReport> {
|
|
133
|
-
const now = o.now ?? Date.now;
|
|
134
|
-
const release = await takeLock(o.background === true || o.wait === false);
|
|
135
|
-
if (!release)
|
|
136
|
-
return {
|
|
137
|
-
busy: true,
|
|
138
|
-
sources: [],
|
|
139
|
-
newlyScored: [],
|
|
140
|
-
noLongerNeeded: [],
|
|
141
|
-
failed: [],
|
|
142
|
-
warnings: [],
|
|
143
|
-
unmatched: {},
|
|
144
|
-
};
|
|
145
|
-
try {
|
|
146
|
-
const before = catalogWith(readDerived(), now());
|
|
147
|
-
const state = readSyncState();
|
|
148
|
-
const key = o.aaKey === undefined ? aaKey() : o.aaKey;
|
|
149
|
-
const t = o.transport ?? {};
|
|
150
|
-
const outcomes = new Map<SourceId, SourceOutcome>();
|
|
151
|
-
const fetchedAt = (id: SourceId) => state.sources[id]?.fetchedAt ?? null;
|
|
152
|
-
const fresh = (id: SourceId) => {
|
|
153
|
-
const at = fetchedAt(id);
|
|
154
|
-
// a cache file that no longer reads (corrupt, or an older schema) is fetched again
|
|
155
|
-
return !o.force && at !== null && now() - Date.parse(at) < TTL_MS && readCached(id) !== null;
|
|
156
|
-
};
|
|
157
|
-
const resting = (id: SourceId) => {
|
|
158
|
-
const s = state.sources[id];
|
|
159
|
-
return o.background && !o.force && s?.error && now() - Date.parse(s.lastAttemptAt) < BACKOFF_MS;
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const run = async (id: SourceId, fetcher: () => Promise<unknown>): Promise<void> => {
|
|
163
|
-
if (fresh(id)) return void outcomes.set(id, { source: id, state: "fresh", fetchedAt: fetchedAt(id) });
|
|
164
|
-
if (resting(id))
|
|
165
|
-
return void outcomes.set(id, {
|
|
166
|
-
source: id,
|
|
167
|
-
state: "skipped",
|
|
168
|
-
fetchedAt: fetchedAt(id),
|
|
169
|
-
detail: "failed within the hour; the next sync tries again",
|
|
170
|
-
});
|
|
171
|
-
const at = now();
|
|
172
|
-
const prev = state.sources[id];
|
|
173
|
-
try {
|
|
174
|
-
const data = await fetcher();
|
|
175
|
-
writeCached(id, data, at);
|
|
176
|
-
const limit = id === "artificial-analysis" ? (data as AaAnswer).rateLimitRemaining : null;
|
|
177
|
-
state.sources[id] = {
|
|
178
|
-
fetchedAt: iso(at),
|
|
179
|
-
lastAttemptAt: iso(at),
|
|
180
|
-
error: null,
|
|
181
|
-
rateLimitRemaining: limit,
|
|
182
|
-
rateLimitAt: limit === null ? null : iso(at),
|
|
183
|
-
};
|
|
184
|
-
outcomes.set(id, { source: id, state: "fetched", fetchedAt: iso(at) });
|
|
185
|
-
} catch (e) {
|
|
186
|
-
const error = errorMessage(e);
|
|
187
|
-
const limit = e instanceof SourceError ? rateLimitRemaining(e.headers) : null;
|
|
188
|
-
// (1.2 minor) a failure with no header keeps the last count with the time that count was read
|
|
189
|
-
state.sources[id] = {
|
|
190
|
-
fetchedAt: prev?.fetchedAt ?? null,
|
|
191
|
-
lastAttemptAt: iso(at),
|
|
192
|
-
error,
|
|
193
|
-
rateLimitRemaining: limit ?? prev?.rateLimitRemaining ?? null,
|
|
194
|
-
rateLimitAt: limit === null ? (prev?.rateLimitAt ?? null) : iso(at),
|
|
195
|
-
};
|
|
196
|
-
outcomes.set(id, { source: id, state: "failed", fetchedAt: prev?.fetchedAt ?? null, error });
|
|
197
|
-
log(o.background ? "debug" : "warn", "sources", { source: id, error });
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const orModels = run("openrouter-models", () => fetchOpenRouterModels(t));
|
|
202
|
-
const jobs: Promise<void>[] = [
|
|
203
|
-
orModels,
|
|
204
|
-
run("models-dev", () => fetchModelsDev(t)),
|
|
205
|
-
run("litellm", () => fetchLiteLlm(t)),
|
|
206
|
-
run("arena", () => fetchArena(t)),
|
|
207
|
-
run("vectara", () => fetchVectara(t)),
|
|
208
|
-
run("epoch", () => fetchEpoch(t)),
|
|
209
|
-
// plan 13 R-H: one request per catalog family OpenRouter lists, so it waits for OpenRouter's model list
|
|
210
|
-
orModels.then(() =>
|
|
211
|
-
run("openrouter-endpoints", async () => {
|
|
212
|
-
const list = readCached("openrouter-models");
|
|
213
|
-
if (!list) throw new Error("no OpenRouter model list to take the ids from");
|
|
214
|
-
const ids = openRouterIds(list.data, { models: shippedModels(), sources: shippedSources() });
|
|
215
|
-
return fetchOpenRouterEndpoints(ids, t);
|
|
216
|
-
}),
|
|
217
|
-
),
|
|
218
|
-
];
|
|
219
|
-
if (key) jobs.push(run("artificial-analysis", () => fetchArtificialAnalysis(key, t)));
|
|
220
|
-
else
|
|
221
|
-
outcomes.set("artificial-analysis", {
|
|
222
|
-
source: "artificial-analysis",
|
|
223
|
-
state: "skipped",
|
|
224
|
-
fetchedAt: fetchedAt("artificial-analysis"),
|
|
225
|
-
detail: "no key: catherd init, or export ARTIFICIAL_ANALYSIS_API_KEY",
|
|
226
|
-
});
|
|
227
|
-
await Promise.all(jobs);
|
|
228
|
-
writeSyncState(state);
|
|
229
|
-
|
|
230
|
-
const changed = [...outcomes.values()].some((x) => x.state === "fetched");
|
|
231
|
-
const raw = cachedAnswers();
|
|
232
|
-
if ((changed || readDerived() === null) && Object.keys(raw).length > 0)
|
|
233
|
-
writeDerived(
|
|
234
|
-
derive(raw, {
|
|
235
|
-
models: shippedModels(),
|
|
236
|
-
scores: shippedScores(),
|
|
237
|
-
sources: shippedSources(),
|
|
238
|
-
now: now(),
|
|
239
|
-
}),
|
|
240
|
-
);
|
|
241
|
-
const derived = readDerived();
|
|
242
|
-
const after = catalogWith(derived, now());
|
|
243
|
-
const had = scored(before);
|
|
244
|
-
const noLongerNeeded = Object.entries(after.treatLike).flatMap(([rung, t]) => {
|
|
245
|
-
if (t.source !== "user") return [];
|
|
246
|
-
const lends = DIMS.filter((d) => after.scores[t.like]?.[d]);
|
|
247
|
-
const covered = (c: Catalog) => lends.length > 0 && lends.every((d) => c.scores[rung]?.[d]);
|
|
248
|
-
return covered(after) && !covered(before) ? [{ rung, like: t.like }] : [];
|
|
249
|
-
});
|
|
250
|
-
const sources = SOURCE_IDS.map((id) => outcomes.get(id)).filter(
|
|
251
|
-
(x): x is SourceOutcome => x !== undefined,
|
|
252
|
-
);
|
|
253
|
-
return {
|
|
254
|
-
busy: false,
|
|
255
|
-
sources,
|
|
256
|
-
newlyScored: [...scored(after)].filter((r) => !had.has(r)).sort(),
|
|
257
|
-
noLongerNeeded,
|
|
258
|
-
failed: sources.flatMap((s) =>
|
|
259
|
-
s.state === "failed" ? [{ source: s.source, error: s.error ?? "" }] : [],
|
|
260
|
-
),
|
|
261
|
-
warnings: derived?.warnings ?? [],
|
|
262
|
-
unmatched: derived?.unmatched ?? {},
|
|
263
|
-
};
|
|
264
|
-
} finally {
|
|
265
|
-
release();
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Spec 1.2 §3.2: the MCP server's boot sync, started without awaiting it. It never throws (a failure is
|
|
271
|
-
* logged at debug) and never waits for another sync. `CATHERD_NO_SYNC=1` turns it off (tests, air-gapped
|
|
272
|
-
* machines), as it does `init`'s sync; `catherd catalog sync` still runs.
|
|
273
|
-
*/
|
|
274
|
-
export function backgroundSync(o: SyncOptions = {}): Promise<SyncReport | null> {
|
|
275
|
-
if (process.env.CATHERD_NO_SYNC === "1") return Promise.resolve(null);
|
|
276
|
-
return syncSources({ ...o, background: true }).catch((e: unknown) => {
|
|
277
|
-
log("debug", "sources", { error: errorMessage(e) });
|
|
278
|
-
return null;
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export interface SourceStatus {
|
|
283
|
-
source: SourceId;
|
|
284
|
-
name: string;
|
|
285
|
-
/** when its cached answer was fetched; null when never */
|
|
286
|
-
fetchedAt: string | null;
|
|
287
|
-
error: string | null;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Spec 1.2 §9 doctor: each source's last fetch and error, the AA key, and AA's requests left. */
|
|
291
|
-
export function sourcesStatus(): {
|
|
292
|
-
sources: SourceStatus[];
|
|
293
|
-
aaKey: boolean;
|
|
294
|
-
rateLimitRemaining: number | null;
|
|
295
|
-
rateLimitAt: string | null;
|
|
296
|
-
} {
|
|
297
|
-
const state = readSyncState();
|
|
298
|
-
const aa = state.sources["artificial-analysis"];
|
|
299
|
-
return {
|
|
300
|
-
sources: shippedSources().sources.map((s) => ({
|
|
301
|
-
source: s.id,
|
|
302
|
-
name: s.name,
|
|
303
|
-
fetchedAt: state.sources[s.id]?.fetchedAt ?? null,
|
|
304
|
-
error: state.sources[s.id]?.error ?? null,
|
|
305
|
-
})),
|
|
306
|
-
aaKey: aaKey() !== null,
|
|
307
|
-
rateLimitRemaining: aa?.rateLimitRemaining ?? null,
|
|
308
|
-
// a state written before 1.5 has no rateLimitAt: its last attempt is the best guess
|
|
309
|
-
rateLimitAt:
|
|
310
|
-
aa?.rateLimitRemaining === null || aa === undefined ? null : (aa.rateLimitAt ?? aa.lastAttemptAt),
|
|
311
|
-
};
|
|
312
|
-
}
|
package/src/services/standins.ts
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Catalog,
|
|
3
|
-
DIMS,
|
|
4
|
-
type Dim,
|
|
5
|
-
type Family,
|
|
6
|
-
type InferredStandIn,
|
|
7
|
-
rungInfo,
|
|
8
|
-
scoresOf,
|
|
9
|
-
} from "../domain/catalog.ts";
|
|
10
|
-
import { catalogRungs } from "../domain/failover.ts";
|
|
11
|
-
import { EFFORT_ORDER } from "../domain/sources.ts";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Spec 1.2 §6.3: the features a rung is compared on. Numbers are z-scored across the catalog; `vendor` and
|
|
15
|
-
* `family` are words, the same or not. Keyless: price, context, release date, vendor and family, the effort
|
|
16
|
-
* (plan 14 Ruling 6), and the rung's own values on each dimension (the Arena and Epoch values, with the
|
|
17
|
-
* shipped ones). With an Artificial Analysis key, its index, `hle`, `scicode`, `lcr`, cost per task and
|
|
18
|
-
* tokens/s join them (`Catalog.features`).
|
|
19
|
-
*/
|
|
20
|
-
export type Features = Record<string, number | string>;
|
|
21
|
-
|
|
22
|
-
/** Spec 1.2 §6.3: a pair sharing fewer features than this is not suggested. */
|
|
23
|
-
export const MIN_SHARED_FEATURES = 3;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Spec 1.5 plan 24: a rung stands in only when it has values of its own on at least this many dimensions. A
|
|
27
|
-
* sparse row (GPT-6.1 Sol's one honesty figure) ranks near everything, for it has few features to differ on,
|
|
28
|
-
* and would lend that one value to unrelated rungs.
|
|
29
|
-
*/
|
|
30
|
-
export const MIN_STANDIN_DIMS = 3;
|
|
31
|
-
|
|
32
|
-
export interface Suggestion {
|
|
33
|
-
/** the canonical rung that would stand in */
|
|
34
|
-
like: string;
|
|
35
|
-
distance: number;
|
|
36
|
-
/** the features the distance rests on: those both rungs have */
|
|
37
|
-
features: string[];
|
|
38
|
-
/** the dimensions it would lend: those the rung lacks and it has */
|
|
39
|
-
lends: Dim[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const DAY_MS = 86_400_000;
|
|
43
|
-
const family = (c: Catalog, canonical: string): Family | null =>
|
|
44
|
-
c.families.find((f) => f.id === canonical.slice(0, canonical.lastIndexOf("#"))) ?? null;
|
|
45
|
-
|
|
46
|
-
/** The rung's own value per dimension: a value it borrows, or catherd's guess (`inferred`), never counts. */
|
|
47
|
-
function ownValues(c: Catalog, canonical: string): Partial<Record<Dim, number>> {
|
|
48
|
-
const out: Partial<Record<Dim, number>> = {};
|
|
49
|
-
for (const d of DIMS) {
|
|
50
|
-
const s = c.scores[canonical]?.[d];
|
|
51
|
-
if (s && s.confidence !== "inferred") out[d] = s.value;
|
|
52
|
-
}
|
|
53
|
-
return out;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Spec 1.2 §6.3: a canonical rung's features, from its family, its effort, its own values and AA's. */
|
|
57
|
-
export function featuresOf(c: Catalog, canonical: string): Features {
|
|
58
|
-
const out: Features = {};
|
|
59
|
-
const f = family(c, canonical);
|
|
60
|
-
if (f) {
|
|
61
|
-
out.price = Math.log10(f.price.input + f.price.output + 1e-6);
|
|
62
|
-
const contexts = Object.values(f.on).flatMap((m) => (m ? [m.context] : []));
|
|
63
|
-
if (contexts.length) out.context = Math.log10(Math.max(...contexts));
|
|
64
|
-
if (f.releaseDate) out.release = Date.parse(f.releaseDate) / DAY_MS;
|
|
65
|
-
const vendor = (f as { vendor?: unknown }).vendor;
|
|
66
|
-
if (typeof vendor === "string") out.vendor = vendor;
|
|
67
|
-
out.family = f.id;
|
|
68
|
-
}
|
|
69
|
-
const effort = (EFFORT_ORDER as readonly string[]).indexOf(canonical.slice(canonical.lastIndexOf("#") + 1));
|
|
70
|
-
if (effort >= 0) out.effort = effort;
|
|
71
|
-
for (const [d, v] of Object.entries(ownValues(c, canonical))) out[d] = v;
|
|
72
|
-
for (const [k, v] of Object.entries(c.features[canonical] ?? {})) out[`aa.${k}`] = v;
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Every canonical rung the catalog can name, and every one it holds a value for. */
|
|
77
|
-
export function canonicalRungs(c: Catalog): string[] {
|
|
78
|
-
const out = new Set(Object.keys(c.scores));
|
|
79
|
-
for (const r of catalogRungs(c)) {
|
|
80
|
-
try {
|
|
81
|
-
out.add(rungInfo(c, r).canonical);
|
|
82
|
-
} catch {
|
|
83
|
-
// catalogRungs only names parseable rungs
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return [...out].sort();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface Scale {
|
|
90
|
-
mean: number;
|
|
91
|
-
sd: number;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Each numeric feature's mean and spread over `all`; a feature with no spread tells rungs apart by nothing. */
|
|
95
|
-
function scales(all: Features[]): Map<string, Scale> {
|
|
96
|
-
const values = new Map<string, number[]>();
|
|
97
|
-
for (const f of all)
|
|
98
|
-
for (const [k, v] of Object.entries(f))
|
|
99
|
-
if (typeof v === "number") values.set(k, [...(values.get(k) ?? []), v]);
|
|
100
|
-
const out = new Map<string, Scale>();
|
|
101
|
-
for (const [k, xs] of values) {
|
|
102
|
-
const mean = xs.reduce((s, x) => s + x, 0) / xs.length;
|
|
103
|
-
const sd = Math.sqrt(xs.reduce((s, x) => s + (x - mean) ** 2, 0) / xs.length);
|
|
104
|
-
if (sd > 0) out.set(k, { mean, sd });
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Spec 1.2 §6.3: the z-scored Euclidean distance over the features both have, as a root mean square so pairs
|
|
111
|
-
* sharing different numbers of features compare (plan 14 Ruling 6); a word counts 0 when equal, else 1. Null
|
|
112
|
-
* when they share fewer than MIN_SHARED_FEATURES.
|
|
113
|
-
*/
|
|
114
|
-
function distance(a: Features, b: Features, z: Map<string, Scale>): { d: number; shared: string[] } | null {
|
|
115
|
-
const shared: string[] = [];
|
|
116
|
-
let sum = 0;
|
|
117
|
-
for (const [k, va] of Object.entries(a)) {
|
|
118
|
-
const vb = b[k];
|
|
119
|
-
if (vb === undefined) continue;
|
|
120
|
-
if (typeof va === "string" || typeof vb === "string") {
|
|
121
|
-
shared.push(k);
|
|
122
|
-
sum += va === vb ? 0 : 1;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
const s = z.get(k);
|
|
126
|
-
if (!s) continue;
|
|
127
|
-
shared.push(k);
|
|
128
|
-
sum += ((va - vb) / s.sd) ** 2;
|
|
129
|
-
}
|
|
130
|
-
if (shared.length < MIN_SHARED_FEATURES) return null;
|
|
131
|
-
return { d: Math.sqrt(sum / shared.length), shared };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** The dimensions the bars use that `canonical` has no value for, of its own or through a treat-like. */
|
|
135
|
-
export function missingDims(c: Catalog, canonical: string, dims: readonly Dim[] = barDimsIn(c)): Dim[] {
|
|
136
|
-
const own = c.scores[canonical] ?? {};
|
|
137
|
-
const like = c.treatLike[canonical]?.like;
|
|
138
|
-
const lent = like ? (c.scores[like] ?? {}) : {};
|
|
139
|
-
return dims.filter((d) => !own[d] && !lent[d]);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Every dimension some bar of the catalog (the defaults, with the user's override) has a threshold on: the
|
|
144
|
-
* dimensions a stand-in fills. No shipped bar uses `steer`, so it is never inferred by default; a user's steer
|
|
145
|
-
* bar makes it one (plan 14 Ruling 7, as the code has always read it; spec 1.5 plan 24 aligns the words).
|
|
146
|
-
*/
|
|
147
|
-
export function barDimsIn(c: Catalog): Dim[] {
|
|
148
|
-
const used = new Set<string>();
|
|
149
|
-
for (const kind of Object.values(c.bars))
|
|
150
|
-
for (const bar of Object.values(kind))
|
|
151
|
-
for (const [d, min] of Object.entries(bar)) if (min !== undefined) used.add(d);
|
|
152
|
-
return DIMS.filter((d) => used.has(d));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
class Ranker {
|
|
156
|
-
private readonly all = new Map<string, Features>();
|
|
157
|
-
private readonly z: Map<string, Scale>;
|
|
158
|
-
constructor(private readonly c: Catalog) {
|
|
159
|
-
for (const r of canonicalRungs(c)) this.all.set(r, featuresOf(c, r));
|
|
160
|
-
this.z = scales([...this.all.values()]);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* The rungs with an own value on any of `dims` (and on at least MIN_STANDIN_DIMS dimensions in all), nearest
|
|
164
|
-
* first; each with what it would lend.
|
|
165
|
-
*/
|
|
166
|
-
rank(canonical: string, dims: readonly Dim[]): Suggestion[] {
|
|
167
|
-
const mine = this.all.get(canonical) ?? featuresOf(this.c, canonical);
|
|
168
|
-
const out: Suggestion[] = [];
|
|
169
|
-
for (const [other, f] of this.all) {
|
|
170
|
-
if (other === canonical) continue;
|
|
171
|
-
const own = ownValues(this.c, other);
|
|
172
|
-
if (Object.keys(own).length < MIN_STANDIN_DIMS) continue;
|
|
173
|
-
const lends = dims.filter((d) => own[d] !== undefined);
|
|
174
|
-
if (lends.length === 0) continue;
|
|
175
|
-
const got = distance(mine, f, this.z);
|
|
176
|
-
if (got) out.push({ like: other, distance: Number(got.d.toFixed(4)), features: got.shared, lends });
|
|
177
|
-
}
|
|
178
|
-
return out.sort((a, b) => a.distance - b.distance || a.like.localeCompare(b.like));
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Spec 1.2 §6.4 `treat-like --suggest`: the `limit` nearest rungs that would lend `canonical` a value it lacks
|
|
184
|
-
* on a dimension the bars use (any scored rung when it lacks none), with their distance and features.
|
|
185
|
-
*/
|
|
186
|
-
export function suggestStandIns(c: Catalog, canonical: string, limit = 3): Suggestion[] {
|
|
187
|
-
const lacking = missingDims(c, canonical);
|
|
188
|
-
return new Ranker(c).rank(canonical, lacking.length ? lacking : DIMS).slice(0, limit);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Spec 1.5 plan 24: the same effort of the family's predecessor (`Family.predecessor`), when it has a value on
|
|
193
|
-
* `d` of its own; null otherwise.
|
|
194
|
-
*/
|
|
195
|
-
function predecessorOf(c: Catalog, canonical: string, d: Dim): { like: string; value: number } | null {
|
|
196
|
-
const pred = family(c, canonical)?.predecessor;
|
|
197
|
-
if (!pred) return null;
|
|
198
|
-
const like = `${pred}${canonical.slice(canonical.lastIndexOf("#"))}`;
|
|
199
|
-
const s = c.scores[like]?.[d];
|
|
200
|
-
return s ? { like, value: s.value } : null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Spec 1.2 §6.1: for every rung the catalog can name, per dimension the bars use that it has no value for
|
|
205
|
-
* (of its own or through a treat-like), its nearest stand-in with a value there. A rung no rung is near
|
|
206
|
-
* enough to (fewer than MIN_SHARED_FEATURES shared) gets none on that dimension. Spec 1.5 plan 24: a new
|
|
207
|
-
* release of a family line takes at least its predecessor's value at the same effort, so a guess never puts it
|
|
208
|
-
* below the model it replaces (the identity run's GPT-6.1 Sol at 37.2, under GPT-6 Luna).
|
|
209
|
-
*/
|
|
210
|
-
export function inferStandIns(c: Catalog): Catalog["inferred"] {
|
|
211
|
-
const dims = barDimsIn(c);
|
|
212
|
-
const ranker = new Ranker(c);
|
|
213
|
-
const out: Catalog["inferred"] = {};
|
|
214
|
-
for (const canonical of canonicalRungs(c)) {
|
|
215
|
-
const lacking = missingDims(c, canonical, dims);
|
|
216
|
-
for (const d of lacking) {
|
|
217
|
-
const best = ranker.rank(canonical, [d])[0];
|
|
218
|
-
const pred = predecessorOf(c, canonical, d);
|
|
219
|
-
const nearest = best ? c.scores[best.like]?.[d]?.value : undefined;
|
|
220
|
-
let entry: InferredStandIn | null = best
|
|
221
|
-
? { like: best.like, distance: best.distance, features: best.features }
|
|
222
|
-
: null;
|
|
223
|
-
if (pred && (nearest === undefined || pred.value >= nearest))
|
|
224
|
-
entry = { like: pred.like, distance: 0, features: ["predecessor"] };
|
|
225
|
-
if (entry) (out[canonical] ??= {})[d] = entry;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return out;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** The catalog with its inferred stand-ins filled in: what routing, validation and the surfaces read. */
|
|
232
|
-
export function withStandIns(c: Catalog): Catalog {
|
|
233
|
-
return { ...c, inferred: inferStandIns(c) };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Whether `canonical` leans on an inferred stand-in for any value (`scoresOf`'s `inferred`). */
|
|
237
|
-
export const leansOnStandIn = (c: Catalog, canonical: string): boolean =>
|
|
238
|
-
(scoresOf(c, canonical)?.inferred.length ?? 0) > 0;
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { OrchestrationHost } from "../domain/host.ts";
|
|
2
|
-
import { type Dim, scoresOf, rungInfo } from "../domain/catalog.ts";
|
|
3
|
-
import { CatherdError } from "../domain/errors.ts";
|
|
4
|
-
import { tryParseRung } from "../domain/ids.ts";
|
|
5
|
-
import { ROLES } from "../domain/roles.ts";
|
|
6
|
-
import { canonicalRung, loadCatalog, readOverride, removeTreatLikes } from "./catalog-service.ts";
|
|
7
|
-
import { getProfile, listProfiles } from "./profile-store.ts";
|
|
8
|
-
import { barDimsIn, type Suggestion, suggestStandIns } from "./standins.ts";
|
|
9
|
-
|
|
10
|
-
/** A profile's rung that would lean on an inferred stand-in, or be left with no value at all (spec 1.2 §6.4). */
|
|
11
|
-
export interface LeftOnStandIn {
|
|
12
|
-
profile: string;
|
|
13
|
-
rung: string;
|
|
14
|
-
/** the dimensions an inferred stand-in would fill; empty when the rung is left unscored */
|
|
15
|
-
dims: Dim[];
|
|
16
|
-
/** no value of its own and no stand-in near enough: routing skips it */
|
|
17
|
-
unscored: boolean;
|
|
18
|
-
/**
|
|
19
|
-
* (1.2 minor) the bar dimensions it keeps no value on at all, though it keeps others: present only when some
|
|
20
|
-
* are; no bar that needs one is cleared
|
|
21
|
-
*/
|
|
22
|
-
missing?: Dim[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Spec 1.2 §6.4: the rungs of every profile (enabled roles' rungs and failover stand-ins) that would lean on an
|
|
27
|
-
* inferred stand-in, or be left with no value at all, once the user's treat-likes for `removed` (canonical
|
|
28
|
-
* rungs, or all of them) are gone. Never removed silently.
|
|
29
|
-
*/
|
|
30
|
-
export function leftOnStandIns(
|
|
31
|
-
removed: string[] | "all",
|
|
32
|
-
host: OrchestrationHost = "unknown",
|
|
33
|
-
): LeftOnStandIn[] {
|
|
34
|
-
const cur = readOverride();
|
|
35
|
-
const treatLike = Object.fromEntries(
|
|
36
|
-
Object.entries(cur.treatLike).filter(([r]) => removed !== "all" && !removed.includes(r)),
|
|
37
|
-
);
|
|
38
|
-
const gone = removed === "all" ? Object.keys(cur.treatLike) : removed;
|
|
39
|
-
const c = loadCatalog({ timings: false, override: { ...cur, treatLike } });
|
|
40
|
-
const out: LeftOnStandIn[] = [];
|
|
41
|
-
// an unknown host resolves no omitted architect/verifier rungs: name what either host would run
|
|
42
|
-
const hosts: OrchestrationHost[] = host === "unknown" ? ["claude-code", "codex"] : [host];
|
|
43
|
-
for (const profile of listProfiles()) {
|
|
44
|
-
const rungs = new Set(
|
|
45
|
-
hosts.flatMap((h) => {
|
|
46
|
-
const p = getProfile(profile, h);
|
|
47
|
-
return [
|
|
48
|
-
...ROLES.filter((r) => p.roles[r].enabled).flatMap((r) => p.roles[r].rungs),
|
|
49
|
-
...Object.values(p.failover),
|
|
50
|
-
];
|
|
51
|
-
}),
|
|
52
|
-
);
|
|
53
|
-
for (const rung of rungs) {
|
|
54
|
-
if (!tryParseRung(rung)) continue;
|
|
55
|
-
const canonical = rungInfo(c, rung).canonical;
|
|
56
|
-
if (!gone.includes(canonical)) continue;
|
|
57
|
-
const s = scoresOf(c, canonical);
|
|
58
|
-
if (!s) {
|
|
59
|
-
out.push({ profile, rung, dims: [], unscored: true });
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
const missing = barDimsIn(c).filter((d) => s.values[d] === undefined);
|
|
63
|
-
if (s.inferred.length || missing.length)
|
|
64
|
-
out.push({
|
|
65
|
-
profile,
|
|
66
|
-
rung,
|
|
67
|
-
dims: s.inferred,
|
|
68
|
-
unscored: false,
|
|
69
|
-
...(missing.length ? { missing } : {}),
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Spec 1.2 §6.4 `--suggest`: the rung, in canonical form, and its three nearest stand-ins. */
|
|
77
|
-
export function suggestFor(rung: string): { canonical: string; lacking: Dim[]; suggestions: Suggestion[] } {
|
|
78
|
-
const c = loadCatalog({ timings: false });
|
|
79
|
-
const canonical = canonicalRung(rung);
|
|
80
|
-
const s = scoresOf(c, canonical);
|
|
81
|
-
return {
|
|
82
|
-
canonical,
|
|
83
|
-
lacking: s ? s.inferred : [],
|
|
84
|
-
suggestions: suggestStandIns(c, canonical),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Spec 1.2 §6.4 `--clear`: removes the user's treat-like for `rung`, naming first the profile rungs it
|
|
90
|
-
* leaves on an inferred stand-in. A shipped treat-like is not the user's to remove.
|
|
91
|
-
*/
|
|
92
|
-
export async function clearTreatLike(
|
|
93
|
-
rung: string,
|
|
94
|
-
host: OrchestrationHost = "unknown",
|
|
95
|
-
): Promise<{ rung: string; like: string; left: LeftOnStandIn[] }> {
|
|
96
|
-
const canonical = canonicalRung(rung);
|
|
97
|
-
const like = readOverride().treatLike[canonical];
|
|
98
|
-
if (!like)
|
|
99
|
-
throw new CatherdError("E_INPUT_INVALID", `${canonical} has no treat-like of yours to clear`, {
|
|
100
|
-
fix: "catherd catalog list shows each rung's treat-like; a shipped one is not yours to clear",
|
|
101
|
-
});
|
|
102
|
-
const left = leftOnStandIns([canonical], host);
|
|
103
|
-
await removeTreatLikes([canonical]);
|
|
104
|
-
return { rung: canonical, like, left };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Spec 1.2 §6.4 `--reset`: removes every treat-like of the user's, naming first the rungs left on a stand-in. */
|
|
108
|
-
export async function resetTreatLikes(
|
|
109
|
-
host: OrchestrationHost = "unknown",
|
|
110
|
-
): Promise<{ removed: [string, string][]; left: LeftOnStandIn[] }> {
|
|
111
|
-
const left = leftOnStandIns("all", host);
|
|
112
|
-
return { removed: await removeTreatLikes("all"), left };
|
|
113
|
-
}
|