返回 CodeWhale
facts-lib.mjs
根目录 / web / scripts / facts-lib.mjs
1 /**
2 * facts-lib.mjs — shared derivation logic for website fact generation and
3 * drift checking. Imported by both derive-facts.mjs (prebuild) and
4 * check-facts.mjs (CI gate).
5 *
6 * Sources of truth:
7 * - <repo>/Cargo.toml → version, workspace crates
8 * - <repo>/crates/tui/src/sandbox/mod.rs → enforced sandbox markers
9 * - <repo>/crates/tui/src/config.rs → provider list (ApiProvider enum), DEFAULT_TEXT_MODEL
10 * - <repo>/npm/codewhale/package.json → node engines
11 * - <repo>/crates/tui/src/tools/*.rs → tool count (ToolSpec impls)
12 * - <repo>/LICENSE → license
13 * - <repo>/web/data/latest-published-release.json → latest published release
14 */
15 import { readFileSync, readdirSync, existsSync } from "node:fs";
16 import { spawnSync } from "node:child_process";
17 import { join, dirname, resolve } from "node:path";
18 import { fileURLToPath } from "node:url";
19
20 const __dirname = dirname(fileURLToPath(import.meta.url));
21 // __dirname is web/scripts; REPO_ROOT is the workspace root (two levels up).
22 export const REPO_ROOT = resolve(__dirname, "..", "..");
23
24 function read(rel) {
25 const p = join(REPO_ROOT, rel);
26 if (!existsSync(p)) return null;
27 return readFileSync(p, "utf-8");
28 }
29
30 export function deriveVersion() {
31 const cargo = read("Cargo.toml");
32 if (!cargo) return null;
33 const m = cargo.match(/^version\s*=\s*"([^"]+)"/m);
34 return m ? m[1] : null;
35 }
36
37 export function deriveCrates() {
38 const cargo = read("Cargo.toml");
39 if (!cargo) return [];
40 const block = cargo.match(/members\s*=\s*\[([\s\S]*?)\]/);
41 if (!block) return [];
42 return [...block[1].matchAll(/"crates\/([^"]+)"/g)].map((m) => m[1]).sort();
43 }
44
45 export function deriveSandboxBackends() {
46 const source = read("crates/tui/src/sandbox/mod.rs");
47 return source ? deriveSandboxBackendsFromSource(source) : [];
48 }
49
50 export function deriveSandboxBackendsFromSource(source) {
51 const marker = source.match(
52 /pub const PUBLIC_SANDBOX_BACKENDS\s*:\s*&\[&str\]\s*=\s*&\[([\s\S]*?)\];/,
53 );
54 if (!marker) return [];
55 return [...marker[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
56 }
57
58 /**
59 * Provider label map — the single source of truth for provider → website
60 * display mapping. MUST be kept in sync with the copy in
61 * web/lib/facts-drift.ts (for the runtime Cloudflare cron path).
62 *
63 * Excluded variants: DeepseekCN (not wired through shared ProviderKind,
64 * #1104), Custom (dynamic meta-provider, #1519), and Antigravity
65 * (a non-runnable legacy config tombstone, permanently excluded from public
66 * provider facts).
67 */
68 const PROVIDER_LABEL_MAP = {
69 Deepseek: { id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" },
70 DeepseekAnthropic: { id: "deepseek-anthropic", label: "DeepSeek Anthropic", env: "DEEPSEEK_API_KEY / ANTHROPIC_API_KEY" },
71 NvidiaNim: { id: "nvidia-nim", label: "NVIDIA NIM", env: "NVIDIA_API_KEY / NVIDIA_NIM_API_KEY" },
72 Openai: { id: "openai", label: "OpenAI-compatible", env: "OPENAI_API_KEY" },
73 Atlascloud: { id: "atlascloud", label: "AtlasCloud", env: "ATLASCLOUD_API_KEY" },
74 WanjieArk: { id: "wanjie-ark", label: "Wanjie Ark", env: "WANJIE_ARK_API_KEY / WANJIE_API_KEY / WANJIE_MAAS_API_KEY" },
75 Volcengine: { id: "volcengine", label: "Volcengine Ark", env: "VOLCENGINE_API_KEY / VOLCENGINE_ARK_API_KEY / ARK_API_KEY" },
76 Openrouter: { id: "openrouter", label: "OpenRouter", env: "OPENROUTER_API_KEY" },
77 Orcarouter: { id: "orcarouter", label: "OrcaRouter", env: "ORCAROUTER_API_KEY" },
78 XiaomiMimo: { id: "xiaomi-mimo", label: "Xiaomi MiMo", env: "XIAOMI_MIMO_TOKEN_PLAN_API_KEY / MIMO_TOKEN_PLAN_API_KEY / XIAOMI_MIMO_API_KEY / XIAOMI_API_KEY / MIMO_API_KEY" },
79 Novita: { id: "novita", label: "Novita AI", env: "NOVITA_API_KEY" },
80 Fireworks: { id: "fireworks", label: "Fireworks AI", env: "FIREWORKS_API_KEY" },
81 Siliconflow: { id: "siliconflow", label: "SiliconFlow", env: "SILICONFLOW_API_KEY" },
82 SiliconflowCn: { id: "siliconflow-CN", label: "SiliconFlow CN", env: "SILICONFLOW_API_KEY" },
83 Arcee: { id: "arcee", label: "Arcee AI", env: "ARCEE_API_KEY" },
84 Moonshot: { id: "moonshot", label: "Moonshot/Kimi", env: "MOONSHOT_API_KEY / KIMI_API_KEY" },
85 Sglang: { id: "sglang", label: "SGLang", env: "SGLANG_API_KEY" },
86 Vllm: { id: "vllm", label: "vLLM", env: "VLLM_API_KEY" },
87 Ollama: { id: "ollama", label: "Ollama", env: "OLLAMA_API_KEY" },
88 OllamaCloud: { id: "ollama-cloud", label: "Ollama Cloud", env: "OLLAMA_CLOUD_API_KEY / OLLAMA_API_KEY" },
89 Huggingface: { id: "huggingface", label: "Hugging Face", env: "HUGGINGFACE_API_KEY / HF_TOKEN" },
90 Modelscope: { id: "modelscope", label: "ModelScope", env: "MODELSCOPE_API_KEY" },
91 Deepinfra: { id: "deepinfra", label: "DeepInfra", env: "DEEPINFRA_API_KEY / DEEPINFRA_TOKEN" },
92 Together: { id: "together", label: "Together AI", env: "TOGETHER_API_KEY" },
93 Qianfan: { id: "qianfan", label: "Baidu Qianfan", env: "QIANFAN_API_KEY / BAIDU_QIANFAN_API_KEY" },
94 OpenaiCodex: { id: "openai-codex", label: "OpenAI Codex", env: "ChatGPT OAuth via `codewhale auth chatgpt`; optional consented Codex CLI credentials (OPENAI_CODEX_ACCESS_TOKEN / CODEX_ACCESS_TOKEN override)" },
95 OpencodeGo: { id: "opencode-go", label: "OpenCode Go", env: "OPENCODE_GO_API_KEY" },
96 OpencodeZen: { id: "opencode-zen", label: "OpenCode Zen", env: "OPENCODE_ZEN_API_KEY / OPENCODE_API_KEY" },
97 Anthropic: { id: "anthropic", label: "Anthropic", env: "ANTHROPIC_API_KEY" },
98 Zai: { id: "zai", label: "Z.ai", env: "ZAI_API_KEY / Z_AI_API_KEY" },
99 Stepfun: { id: "stepfun", label: "StepFun", env: "STEPFUN_API_KEY / STEP_API_KEY" },
100 Minimax: { id: "minimax", label: "MiniMax", env: "MINIMAX_API_KEY" },
101 MinimaxAnthropic: { id: "minimax-anthropic", label: "MiniMax (Anthropic-compatible)", env: "MINIMAX_API_KEY" },
102 Openmodel: { id: "openmodel", label: "OpenModel", env: "OPENMODEL_API_KEY" },
103 Sakana: { id: "sakana", label: "Sakana AI", env: "FUGU_API_KEY / SAKANA_API_KEY" },
104 LongCat: { id: "longcat", label: "Meituan LongCat", env: "LONGCAT_API_KEY" },
105 Meta: { id: "meta", label: "Meta Model API", env: "META_MODEL_API_KEY / MODEL_API_KEY" },
106 Telecomjs: { id: "telecomjs", label: "TelecomJS TokenHub", env: "TELECOMJS_API_KEY" },
107 Xai: { id: "xai", label: "xAI", env: "XAI_API_KEY" },
108 Mistral: { id: "mistral", label: "Mistral AI", env: "MISTRAL_API_KEY" },
109 Google: { id: "google", label: "Google Gemini", env: "GOOGLE_API_KEY / GEMINI_API_KEY" },
110 Edenai: { id: "edenai", label: "Eden AI", env: "EDENAI_API_KEY" },
111 Concentrate: { id: "concentrate", label: "Concentrate", env: "CONCENTRATE_API_KEY" },
112 Codewhale: { id: "codewhale", label: "Codewhale", env: "CODEWHALE_API_KEY" },
113 ModelstudioTokenPlan: { id: "modelstudio-token-plan", label: "Model Studio Token Plan", env: "MODELSTUDIO_API_KEY" },
114 ModelstudioTokenPlanAnthropic: { id: "modelstudio-token-plan-anthropic", label: "Model Studio Token Plan (Anthropic-compatible)", env: "MODELSTUDIO_API_KEY" },
115 ModelstudioCodingPlan: { id: "modelstudio-coding-plan", label: "Model Studio Coding Plan", env: "MODELSTUDIO_API_KEY" },
116 ModelstudioCodingPlanAnthropic: { id: "modelstudio-coding-plan-anthropic", label: "Model Studio Coding Plan (Anthropic-compatible)", env: "MODELSTUDIO_API_KEY" },
117 Zenmux: { id: "zenmux", label: "ZenMux", env: "ZENMUX_API_KEY" },
118 Csdn: { id: "csdn", label: "CSDN 星图 (Starmap)", env: "CSDN_API_KEY" },
119 };
120
121 // DeepseekCN: not wired through shared ProviderKind (#1104).
122 // Custom: the dynamic OpenAI-compatible meta-provider (#1519) — a runtime
123 // catch-all for user-defined endpoints, not a website-listable provider.
124 // Antigravity is a non-runnable legacy config tombstone, never a website
125 // provider.
126 const EXCLUDED_PROVIDERS = new Set(["DeepseekCN", "Custom", "Antigravity"]);
127
128 function providerEnumVariants() {
129 const cfg = read("crates/tui/src/config.rs");
130 if (!cfg) return [];
131 const enumBlock = cfg.match(/pub enum ApiProvider \{([\s\S]*?)\}/);
132 if (!enumBlock) return [];
133 return [...enumBlock[1].matchAll(/^\s*(\w+)\s*,\s*$/gm)].map((m) => m[1]);
134 }
135
136 /**
137 * ApiProvider variants that are neither mapped to a website label nor
138 * intentionally excluded. Exposed so the CI gate (`check-facts.mjs`) can
139 * hard-fail on provider-inventory drift (#3772); the generator stays lenient
140 * and merely warns so local `prebuild` is not blocked mid-development.
141 */
142 export function unmappedProviderVariants() {
143 return providerEnumVariants().filter(
144 (v) => !EXCLUDED_PROVIDERS.has(v) && !PROVIDER_LABEL_MAP[v],
145 );
146 }
147
148 export function deriveProviders() {
149 const variants = providerEnumVariants();
150
151 const unmapped = unmappedProviderVariants();
152 if (unmapped.length > 0) {
153 console.error(
154 `[facts-lib] ApiProvider variants missing from PROVIDER_LABEL_MAP: ${unmapped.join(", ")}. ` +
155 "Add them to PROVIDER_LABEL_MAP here AND in web/lib/facts-drift.ts (or to EXCLUDED_PROVIDERS if intentionally hidden).",
156 );
157 // The generator stays lenient and returns what it can map; the hard gate
158 // lives in check-facts.mjs via unmappedProviderVariants() (#3772).
159 }
160 return variants
161 .filter((v) => !EXCLUDED_PROVIDERS.has(v))
162 .map((v) => PROVIDER_LABEL_MAP[v])
163 .filter(Boolean);
164 }
165
166 export function deriveDefaultModel() {
167 // DEFAULT_TEXT_MODEL's definition moved to config/models.rs in the #3311 split;
168 // read both and match the const *definition* specifically (`= "..."`) so we
169 // don't mis-bind to a later string at a mere use site.
170 const cfg =
171 (read("crates/tui/src/config/models.rs") ?? "") +
172 "\n" +
173 (read("crates/tui/src/config.rs") ?? "");
174 if (!cfg.trim()) return null;
175 const m = cfg.match(/DEFAULT_TEXT_MODEL\s*(?::\s*&str\s*)?=\s*"([^"]+)"/);
176 return m ? m[1] : null;
177 }
178
179 export function deriveNodeEngines() {
180 const pkg = read("npm/codewhale/package.json");
181 if (!pkg) return null;
182 try {
183 return JSON.parse(pkg).engines?.node ?? null;
184 } catch {
185 return null;
186 }
187 }
188
189 // --- Models ---------------------------------------------------------------
190 //
191 // The public "which models" list. Two source files are read, never scraped
192 // from live provider APIs:
193 //
194 // crates/tui/src/model_registry.rs → SEED_MODEL_IDS tuples carry
195 // the canonical model ids Codewhale makes first-class promises about,
196 // each with its coarse ModelProvider grouping.
197 // crates/models/assets/model_catalog.bundled.json → the bundled metadata
198 // snapshot (context window, max output, reasoning flag) that the
199 // runtime's offline catalog layer ships.
200 //
201 // Provider-prefixed spellings of the same model (`deepseek/`,
202 // `deepseek-ai/`, `z-ai/`, `moonshotai/`, `minimax/`, `qwen/`, `arcee-ai/`,
203 // `opencode-go/`, `nvidia/`) collapse into one canonical row — they are wire
204 // aliases for one model, not separate models.
205 //
206 // `addedAt` is the honest recency the repo itself can prove: the commit date
207 // on which the model id first appeared in the model *declaration* paths
208 // below — where a model becomes selectable, not where it is mentioned in
209 // tests or docs. It means "first supported in Codewhale source", which is
210 // the claim the page makes; it is NOT the provider's own release date.
211
212 const MODEL_REGISTRY_PATH = "crates/tui/src/model_registry.rs";
213 const MODEL_CATALOG_PATH = "crates/models/assets/model_catalog.bundled.json";
214 const MODEL_DECLARATION_PATHS = [
215 "crates/config",
216 "crates/models",
217 "crates/tui/src/model_registry.rs",
218 "crates/tui/src/config",
219 "crates/tui/src/model_routing.rs",
220 ];
221
222 const MODEL_PROVIDER_LABELS = {
223 DeepSeek: "DeepSeek",
224 Anthropic: "Anthropic",
225 OpenAi: "OpenAI",
226 OpenAiCodex: "OpenAI Codex",
227 Moonshot: "Moonshot/Kimi",
228 Zai: "Z.ai",
229 Minimax: "MiniMax",
230 Stepfun: "StepFun",
231 Qwen: "Qwen",
232 Arcee: "Arcee",
233 Together: "Together",
234 XiaomiMimo: "Xiaomi MiMo",
235 Meta: "Meta",
236 Xai: "xAI",
237 Mistral: "Mistral",
238 Google: "Google",
239 Other: null,
240 };
241
242 // Longest prefixes first so `deepseek-ai/` wins over `deepseek/`.
243 const MODEL_ID_PREFIXES = [
244 "deepseek-ai", "moonshotai", "opencode-go", "arcee-ai", "minimax",
245 "deepseek", "huggingface", "together", "nvidia", "qwen", "z-ai",
246 "openai", "google", "xai", "mistral", "stepfun", "meta",
247 ];
248
249 const MODEL_PREFIX_PROVIDERS = {
250 "deepseek-ai": "DeepSeek", deepseek: "DeepSeek", "z-ai": "Zai",
251 moonshotai: "Moonshot", minimax: "Minimax", qwen: "Qwen",
252 "arcee-ai": "Arcee", together: "Together", nvidia: "Other",
253 "opencode-go": "Moonshot", openai: "OpenAi", google: "Google",
254 xai: "Xai", mistral: "Mistral", stepfun: "Stepfun", meta: "Meta",
255 huggingface: "Other",
256 };
257
258 function canonicalModelId(id) {
259 const slash = id.indexOf("/");
260 if (slash < 0) return { canonical: id, prefix: null };
261 const prefix = id.slice(0, slash);
262 return MODEL_ID_PREFIXES.includes(prefix)
263 ? { canonical: id.slice(slash + 1), prefix }
264 : { canonical: id, prefix: null };
265 }
266
267 // Catalog-only ids carry no provider prefix; the model family is still
268 // obvious from the name for these. Everything else stays "Other" and renders
269 // as "—" rather than a guessed attribution.
270 const MODEL_NAME_FAMILIES = [
271 [/^deepseek/i, "DeepSeek"],
272 [/^claude/i, "Anthropic"],
273 [/^(gpt|o[0-9]|codex|chatgpt)/i, "OpenAi"],
274 [/^(kimi|moonshot)/i, "Moonshot"],
275 [/^(glm-|zai)/i, "Zai"],
276 [/^(minimax|abab)/i, "Minimax"],
277 [/^step(?:-|audio)/i, "Stepfun"],
278 [/^(qwen|qwq)/i, "Qwen"],
279 [/^(arcee|trinity|afm|virtuoso|maestro|spotlight|blitz)/i, "Arcee"],
280 [/^(mimo|xiaomi)/i, "XiaomiMimo"],
281 [/^(llama|meta-llama)/i, "Meta"],
282 [/^grok/i, "Xai"],
283 [/^(mistral|codestral|devstral|magistral|mixtral|pixtral|ministral|voxtral)/i, "Mistral"],
284 [/^(gemini|gemma|learnlm)/i, "Google"],
285 ];
286
287 function inferModelFamily(canonical) {
288 for (const [re, provider] of MODEL_NAME_FAMILIES) {
289 if (re.test(canonical)) return provider;
290 }
291 return "Other";
292 }
293
294 function seedModelRows() {
295 const src = read(MODEL_REGISTRY_PATH);
296 if (!src) return [];
297 return [...src.matchAll(/\("([^"]+)",\s*ModelProvider::(\w+)\)/g)]
298 .map((m) => ({ id: m[1], provider: m[2] }));
299 }
300
301 function bundledCatalogEntries() {
302 const raw = read(MODEL_CATALOG_PATH);
303 if (!raw) return new Map();
304 try {
305 const parsed = JSON.parse(raw);
306 return parsed && typeof parsed === "object" && parsed.entries
307 ? new Map(Object.entries(parsed.entries))
308 : new Map();
309 } catch {
310 return new Map();
311 }
312 }
313
314 /**
315 * First-appearance dates for every model alias in ONE history pass. The
316 * `-G` alternation restricts emitted diffs to commits that touched a model
317 * id line, keeping the output small; walking it oldest-first and matching
318 * `"id"` on added lines gives each alias its introduction commit. A commit
319 * that only removed an id can never be its earliest hit.
320 */
321 function modelFirstSeen(aliases) {
322 const needles = [...new Set(aliases)].map((id) =>
323 `"${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`,
324 );
325 if (needles.length === 0) return new Map();
326 const res = spawnSync(
327 "git",
328 [
329 "log", "--reverse", "--format=%x00%cI", "--no-renames",
330 "-G", needles.join("|"),
331 "-p", "--", ...MODEL_DECLARATION_PATHS,
332 ],
333 { cwd: REPO_ROOT, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 },
334 );
335 const firstSeen = new Map();
336 if (res.status !== 0 || typeof res.stdout !== "string") return firstSeen;
337 for (const chunk of res.stdout.split("\0").slice(1)) {
338 const nl = chunk.indexOf("\n");
339 const when = chunk.slice(0, nl).trim().slice(0, 10);
340 const added = chunk
341 .split("\n")
342 .filter((line) => line.startsWith("+") && !line.startsWith("+++"))
343 .join("\n");
344 for (const id of aliases) {
345 if (!firstSeen.has(id) && added.includes(`"${id}"`)) {
346 firstSeen.set(id, when);
347 }
348 }
349 }
350 return firstSeen;
351 }
352
353 export function deriveModels() {
354 const catalog = bundledCatalogEntries();
355
356 // Canonical id → merged row.
357 const rows = new Map();
358 const note = (rawId, providerKey) => {
359 const { canonical, prefix } = canonicalModelId(rawId);
360 const existing = rows.get(canonical);
361 const provider =
362 providerKey ??
363 MODEL_PREFIX_PROVIDERS[prefix] ??
364 inferModelFamily(canonical);
365 if (existing) {
366 if (existing.provider === "Other" && provider !== "Other") {
367 existing.provider = provider;
368 }
369 existing.catalogIds.push(rawId);
370 return existing;
371 }
372 const row = { id: canonical, provider, catalogIds: [rawId] };
373 rows.set(canonical, row);
374 return row;
375 };
376
377 for (const seed of seedModelRows()) {
378 note(seed.id, seed.provider);
379 }
380 for (const [id, entry] of catalog) {
381 const row = note(id, null);
382 row.entry = row.entry ?? entry;
383 }
384
385 // A model's date is the earliest first-appearance across all of its wire
386 // spellings — an alias arriving later must not move the model's date.
387 const allAliases = [...rows.values()].flatMap((row) => row.catalogIds);
388 const firstSeen = modelFirstSeen(allAliases);
389 const models = [...rows.values()].map((row) => {
390 const seen = row.catalogIds
391 .map((cid) => firstSeen.get(cid))
392 .filter(Boolean)
393 .sort();
394 return {
395 id: row.id,
396 provider: MODEL_PROVIDER_LABELS[row.provider] ?? null,
397 contextWindow: row.entry?.context_window ?? null,
398 maxOutput: row.entry?.max_output ?? null,
399 reasoning: row.entry?.supports_reasoning === true,
400 addedAt: seen[0] ?? null,
401 };
402 });
403
404 // Newest first; undated rows sink to the end alphabetically.
405 models.sort((a, b) => {
406 if (a.addedAt && b.addedAt && a.addedAt !== b.addedAt) {
407 return b.addedAt.localeCompare(a.addedAt);
408 }
409 if (a.addedAt !== b.addedAt) return a.addedAt ? -1 : 1;
410 return a.id.localeCompare(b.id);
411 });
412 return models;
413 }
414
415 export function deriveToolCount() {
416 const dir = join(REPO_ROOT, "crates/tui/src/tools");
417 if (!existsSync(dir)) return null;
418 let count = 0;
419 for (const f of readdirSync(dir)) {
420 if (!f.endsWith(".rs")) continue;
421 const body = readFileSync(join(dir, f), "utf-8");
422 count += (body.match(/^impl ToolSpec for /gm) ?? []).length;
423 }
424 return count > 0 ? count : null;
425 }
426
427 export function deriveLicense() {
428 const lic = read("LICENSE");
429 if (!lic) return null;
430 const first = lic.split(/\r?\n/).find((l) => l.trim().length > 0);
431 if (!first) return null;
432 if (/^MIT License/i.test(first)) return "MIT";
433 if (/Apache.*2\.0/i.test(first)) return "Apache-2.0";
434 return first.trim();
435 }
436
437 export function deriveLatestPublishedRelease() {
438 const raw = read("web/data/latest-published-release.json");
439 if (!raw) return null;
440 try {
441 const release = JSON.parse(raw);
442 if (
443 typeof release.tag !== "string" ||
444 typeof release.version !== "string" ||
445 release.tag !== `v${release.version}` ||
446 typeof release.publishedAt !== "string" ||
447 !Number.isFinite(Date.parse(release.publishedAt)) ||
448 typeof release.url !== "string" ||
449 release.url !== `https://github.com/Hmbown/CodeWhale/releases/tag/${release.tag}`
450 ) {
451 return null;
452 }
453 return release;
454 } catch {
455 return null;
456 }
457 }
458
459 /**
460 * Re-derive all mechanical facts from the current workspace. The returned
461 * object is the same shape as web/lib/facts.generated.ts → RepoFacts.
462 */
463 export function buildFacts() {
464 const providers = deriveProviders();
465 // In check mode, missing provider mappings are a warning, not a crash.
466 // But if we truly have zero mapped providers, that signals something
467 // went wrong (e.g. config.rs renamed) — still return an empty array
468 // rather than null so the checker can report it.
469
470 const facts = {
471 generatedAt: new Date().toISOString(),
472 // next.config.ts injects these from the exact checkout into the built
473 // artifact. They stay null in the tracked snapshot to avoid a
474 // self-referential generated-file diff after every commit.
475 sourceRevision: null,
476 sourceCommittedAt: null,
477 version: deriveVersion(),
478 crates: deriveCrates(),
479 sandboxBackends: deriveSandboxBackends(),
480 providers,
481 models: deriveModels(),
482 defaultModel: deriveDefaultModel(),
483 nodeEngines: deriveNodeEngines(),
484 toolCount: deriveToolCount(),
485 license: deriveLicense(),
486 latestPublishedRelease: deriveLatestPublishedRelease(),
487 };
488
489 return facts;
490 }
491
491 lines Plain Text