204 lines
8.1 KiB
JavaScript
Executable File
204 lines
8.1 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Static guard for the cookbook deployment/playground engines and their configs.
|
|
// Zero dependencies, no browser, no Mintlify — plain `node`.
|
|
//
|
|
// node docs/scripts/check_cookbook_configs.mjs
|
|
//
|
|
// What it protects, in order of how expensive the bug is to find by hand:
|
|
//
|
|
// 1. MIRROR drift. The overlay-resolution rule is written in both engines
|
|
// because Mintlify snippets cannot import each other. If the copies drift,
|
|
// the Deploy command and the playground's base disagree and the reader sees
|
|
// phantom +/- lines in the diff — with no error anywhere.
|
|
// 2. Sibling identity. Overlay resolution clones the base cell, so sibling
|
|
// detection must compare match dimensions rather than object references.
|
|
// 3. Config/engine contract. A cell keyed on a dimension the config no longer
|
|
// declares silently stops matching; the panel just shows a different cell.
|
|
// 4. Predicate safety. showWhen / disabled / flags run against selections the
|
|
// author never clicked through; a throw there blanks the whole widget.
|
|
|
|
import { readFileSync, readdirSync } from "node:fs";
|
|
import { dirname, join, relative } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const SNIPPETS = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "snippets");
|
|
const CONFIGS = join(SNIPPETS, "configs");
|
|
const LEGACY_DIMS = ["variants", "quantizations", "strategies", "nodesOptions"];
|
|
|
|
const failures = [];
|
|
const fail = (where, msg) => failures.push(`${where}: ${msg}`);
|
|
|
|
// ---------------------------------------------------------------- 1. MIRROR
|
|
// Compare the marked blocks with comments and whitespace normalized away, so
|
|
// wording may differ per file but the rule may not.
|
|
const mirrorBody = (file) => {
|
|
const src = readFileSync(join(SNIPPETS, file), "utf8");
|
|
const start = src.indexOf("==== MIRROR");
|
|
const end = src.indexOf("==== end MIRROR");
|
|
if (start === -1 || end === -1) return null;
|
|
return src
|
|
.slice(src.indexOf("\n", start), end)
|
|
.split("\n")
|
|
.map((l) => l.trim())
|
|
.filter((l) => l && !l.startsWith("//"))
|
|
.join(" ")
|
|
.replace(/\s+/g, " ");
|
|
};
|
|
|
|
const a = mirrorBody("_deployment.jsx");
|
|
const b = mirrorBody("_playground.jsx");
|
|
if (a === null) fail("_deployment.jsx", "MIRROR markers missing");
|
|
if (b === null) fail("_playground.jsx", "MIRROR markers missing");
|
|
if (a && b && a !== b) {
|
|
fail("MIRROR", "overlay resolution has drifted between the two engines");
|
|
const [la, lb] = [a.split(" "), b.split(" ")];
|
|
const i = la.findIndex((t, k) => t !== lb[k]);
|
|
fail("MIRROR", `first divergence near token ${i}: `
|
|
+ `_deployment "${la.slice(i, i + 8).join(" ")}" vs `
|
|
+ `_playground "${lb.slice(i, i + 8).join(" ")}"`);
|
|
}
|
|
|
|
// `withOverlay` returns a clone, so object identity can never distinguish the
|
|
// current base cell from a true sibling. This previously made every cookbook
|
|
// show a spurious "matches … / switch base" hint before the reader changed
|
|
// anything.
|
|
const playgroundSource = readFileSync(join(SNIPPETS, "_playground.jsx"), "utf8");
|
|
if (/\bmatchedCell\s*!==\s*baseCell\b/.test(playgroundSource)) {
|
|
fail("_playground.jsx", "sibling detection compares cloned cells by object identity");
|
|
}
|
|
|
|
// --------------------------------------------------------------- 3/4. Configs
|
|
// Configs are .jsx with a single `export const config` literal; import them
|
|
// through a data: URL so no temp file is needed.
|
|
const loadConfig = async (path) => {
|
|
const src = readFileSync(path, "utf8");
|
|
const mod = await import(
|
|
"data:text/javascript," + encodeURIComponent(src)
|
|
);
|
|
return mod.config;
|
|
};
|
|
|
|
// Every combination of match dims + overlay dims the reader can produce.
|
|
const selectionSpace = (config) => {
|
|
const dims = [
|
|
{ id: "hw", options: (config.supportedHardware || []).map((id) => ({ id })) },
|
|
...(config.matchDims || []),
|
|
...(config.overlayDims || []),
|
|
];
|
|
let space = [{}];
|
|
for (const d of dims) {
|
|
const next = [];
|
|
for (const partial of space) {
|
|
for (const opt of (d.options || [])) next.push({ ...partial, [d.id]: opt.id });
|
|
}
|
|
space = next.length ? next : space;
|
|
if (space.length > 20000) return space.slice(0, 20000); // cheap blow-up guard
|
|
}
|
|
return space;
|
|
};
|
|
|
|
const walk = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
|
|
e.isDirectory() ? walk(join(dir, e.name))
|
|
: (e.name.endsWith(".jsx")
|
|
&& !e.name.includes("benchmark")
|
|
&& e.name !== "popular-models.jsx"
|
|
? [join(dir, e.name)] : []));
|
|
|
|
for (const path of walk(CONFIGS)) {
|
|
const where = relative(join(SNIPPETS, ".."), path);
|
|
let config;
|
|
try {
|
|
config = await loadConfig(path);
|
|
} catch (e) {
|
|
fail(where, `does not parse as a module: ${e.message}`);
|
|
continue;
|
|
}
|
|
if (!config) { fail(where, "no `export const config`"); continue; }
|
|
|
|
const custom = Array.isArray(config.matchDims);
|
|
|
|
// A config either declares its own dims or carries the full legacy set —
|
|
// half of each means the engine silently renders a dimension nobody authored.
|
|
if (!custom) {
|
|
for (const k of LEGACY_DIMS) {
|
|
if (!Array.isArray(config[k])) fail(where, `legacy config is missing \`${k}\``);
|
|
}
|
|
}
|
|
|
|
const matchIds = ["hw", ...(custom
|
|
? config.matchDims.map((d) => d.id)
|
|
: LEGACY_DIMS.map((k) => ({ variants: "variant", quantizations: "quant",
|
|
strategies: "strategy", nodesOptions: "nodes" })[k]))];
|
|
|
|
for (const [i, cell] of (config.cells || []).entries()) {
|
|
const keys = Object.keys(cell.match || {}).sort();
|
|
const want = [...matchIds].sort();
|
|
if (keys.join(",") !== want.join(",")) {
|
|
fail(where, `cells[${i}].match keys [${keys}] != declared dims [${want}]`);
|
|
}
|
|
for (const dim of (config.matchDims || [])) {
|
|
const v = cell.match[dim.id];
|
|
if (!(dim.options || []).some((o) => o.id === v)) {
|
|
fail(where, `cells[${i}].match.${dim.id}="${v}" is not an option of that dim`);
|
|
}
|
|
}
|
|
// Without a `nodes` dim the node count rides on the cell; a missing one
|
|
// silently degrades a multi-node recipe to single-node.
|
|
if (custom && !matchIds.includes("nodes") && cell.nnodes === undefined) {
|
|
fail(where, `cells[${i}] has no \`nnodes\` and the config declares no nodes dim`);
|
|
}
|
|
}
|
|
|
|
for (const dim of (config.overlayDims || [])) {
|
|
const ids = (dim.options || []).map((o) => o.id);
|
|
if (dim.default !== undefined && !ids.includes(dim.default)) {
|
|
fail(where, `overlayDims.${dim.id}.default="${dim.default}" is not one of [${ids}]`);
|
|
}
|
|
}
|
|
|
|
// Predicates and flag builders must survive every reachable selection.
|
|
const space = selectionSpace(config);
|
|
const probe = (fn, label) => {
|
|
for (const sel of space) {
|
|
try { fn(sel); } catch (e) {
|
|
fail(where, `${label} throws on ${JSON.stringify(sel)}: ${e.message}`);
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
for (const dim of [...(config.matchDims || []), ...(config.overlayDims || [])]) {
|
|
if (typeof dim.showWhen === "function") probe(dim.showWhen, `${dim.id}.showWhen`);
|
|
for (const opt of (dim.options || [])) {
|
|
const tag = `${dim.id}.${opt.id}`;
|
|
if (typeof opt.showWhen === "function") probe(opt.showWhen, `${tag}.showWhen`);
|
|
if (typeof opt.disabled === "function") probe(opt.disabled, `${tag}.disabled`);
|
|
for (const key of ["flags", "env", "hints"]) {
|
|
if (typeof opt[key] !== "function") continue;
|
|
probe((sel) => {
|
|
const out = opt[key](sel);
|
|
if (out !== undefined && !Array.isArray(out)) throw new Error(`${key} returned ${typeof out}, expected an array`);
|
|
for (const f of (out || [])) {
|
|
if (typeof f !== "string") throw new Error(`${key} yielded a non-string entry`);
|
|
if (/undefined|NaN/.test(f)) throw new Error(`${key} produced "${f}"`);
|
|
}
|
|
}, `${tag}.${key}`);
|
|
}
|
|
}
|
|
}
|
|
if (typeof config.curl === "function") {
|
|
probe((sel) => {
|
|
const out = config.curl(sel, null);
|
|
if (typeof out !== "string") {
|
|
throw new Error(`curl returned ${typeof out}, expected a string`);
|
|
}
|
|
}, "curl");
|
|
}
|
|
}
|
|
|
|
if (failures.length) {
|
|
console.error(`FAIL (${failures.length})`);
|
|
for (const f of failures) console.error(" - " + f);
|
|
process.exit(1);
|
|
}
|
|
console.log("cookbook config check: OK");
|