Spaces:
Runtime error
Runtime error
| // Validates that count-based assertions in docs match the actual code state. | |
| // | |
| // Two tiers of checks: | |
| // • STRICT (always blocking — exit 1 on drift): high-confidence, slow-moving counts | |
| // that historically caused the worst drift across README / AGENTS / docs. | |
| // - provider count (source of truth: docs/reference/PROVIDER_REFERENCE.md total, | |
| // which is auto-generated from src/shared/constants/providers.ts) | |
| // - i18n locale count (source of truth: config/i18n.json `locales`) | |
| // • SOFT (heuristic — only fails with --strict): file-count based assertions that can | |
| // false-positive. | |
| // - executors count in open-sse/executors/ | |
| // - routing strategies in src/shared/constants/routingStrategies.ts | |
| // - OAuth providers in src/lib/oauth/providers/ | |
| // - A2A skills in src/lib/a2a/skills/ | |
| // - Cloud agents in src/lib/cloudAgent/agents/ | |
| // | |
| // Exits 0 on success, 1 on STRICT drift (or any drift with --strict). | |
| // Run: node scripts/check/check-docs-counts-sync.mjs | |
| // | |
| // NOTE: the provider check trusts PROVIDER_REFERENCE.md as the canonical total. If a | |
| // provider is added to the code but the reference is not regenerated, this guard will | |
| // not catch it — regenerate with `npm run gen:provider-reference` before relying on it. | |
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import { fileURLToPath } from "node:url"; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const ROOT = path.resolve(__dirname, "..", ".."); | |
| const COMMON_NON_IMPL_BASENAMES = new Set([ | |
| "index.ts", | |
| "index.mts", | |
| "types.ts", | |
| "base.ts", | |
| "constants.ts", | |
| ]); | |
| function countFiles(dir, suffix = ".ts") { | |
| const abs = path.join(ROOT, dir); | |
| if (!fs.existsSync(abs)) return 0; | |
| return fs | |
| .readdirSync(abs) | |
| .filter( | |
| (f) => | |
| f.endsWith(suffix) && | |
| !f.endsWith(".test.ts") && | |
| !f.startsWith("__") && | |
| !COMMON_NON_IMPL_BASENAMES.has(f) | |
| ).length; | |
| } | |
| function countRoutingStrategies() { | |
| const file = path.join(ROOT, "src", "shared", "constants", "routingStrategies.ts"); | |
| if (!fs.existsSync(file)) return 0; | |
| const txt = fs.readFileSync(file, "utf8"); | |
| const m = txt.match(/ROUTING_STRATEGY_VALUES\s*=\s*\[([^\]]*)\]/); | |
| if (!m) return 0; | |
| return (m[1].match(/"[^"]+"/g) || []).length; | |
| } | |
| // PURE: parse the canonical provider total out of the auto-generated catalog text. | |
| export function parseProviderTotal(referenceText) { | |
| if (!referenceText) return 0; | |
| const m = referenceText.match(/Total providers:\s*\*\*(\d+)\*\*/); | |
| return m ? Number(m[1]) : 0; | |
| } | |
| // STRICT: canonical provider total, read from the auto-generated catalog. | |
| export function readProviderTotal() { | |
| const abs = path.join(ROOT, "docs", "reference", "PROVIDER_REFERENCE.md"); | |
| if (!fs.existsSync(abs)) return 0; | |
| return parseProviderTotal(fs.readFileSync(abs, "utf8")); | |
| } | |
| // STRICT: canonical i18n locale count, read from the shared config. | |
| export function countLocales() { | |
| const abs = path.join(ROOT, "config", "i18n.json"); | |
| if (!fs.existsSync(abs)) return 0; | |
| try { | |
| const cfg = JSON.parse(fs.readFileSync(abs, "utf8")); | |
| return Array.isArray(cfg.locales) ? cfg.locales.length : 0; | |
| } catch { | |
| return 0; | |
| } | |
| } | |
| // PURE: tally STRICT vs SOFT drift for a list of checks, given a content lookup. | |
| // `getContent(file) -> string | null`. A check whose `actual` is 0 is skipped (the | |
| // source count could not be determined). Returns { strict, soft, lines }. | |
| export function tallyDrift(checks, getContent) { | |
| let strict = 0; | |
| let soft = 0; | |
| const lines = []; | |
| for (const c of checks) { | |
| const tier = c.strict ? "STRICT" : "soft"; | |
| lines.push(`\n• ${c.label}: ${c.actual} (real) [${tier}]`); | |
| if (!c.actual) { | |
| lines.push(` ⚠ could not determine ${c.docKey} count from source — skipping`); | |
| continue; | |
| } | |
| for (const f of c.files) { | |
| const content = getContent(f); | |
| const found = content != null && content.includes(String(c.actual)); | |
| if (found) { | |
| lines.push(` ✓ ${f} mentions "${c.actual}"`); | |
| } else { | |
| lines.push(` ${c.strict ? "✗" : "⚠"} ${f} does NOT mention "${c.actual}" for ${c.docKey}`); | |
| if (c.strict) strict++; | |
| else soft++; | |
| } | |
| } | |
| } | |
| return { strict, soft, lines }; | |
| } | |
| export function buildChecks() { | |
| return [ | |
| { | |
| label: "Provider count", | |
| actual: readProviderTotal(), | |
| docKey: "providers", | |
| strict: true, | |
| files: ["README.md", "AGENTS.md", "CLAUDE.md"], | |
| }, | |
| { | |
| label: "i18n locales count", | |
| actual: countLocales(), | |
| docKey: "i18n locales", | |
| strict: true, | |
| files: ["docs/README.md", "docs/guides/I18N.md", "AGENTS.md"], | |
| }, | |
| { | |
| label: "Executors count", | |
| actual: countFiles("open-sse/executors"), | |
| docKey: "executors", | |
| strict: false, | |
| files: ["docs/architecture/ARCHITECTURE.md", "docs/architecture/CODEBASE_DOCUMENTATION.md"], | |
| }, | |
| { | |
| label: "Routing strategies count", | |
| actual: countRoutingStrategies(), | |
| docKey: "strategies", | |
| strict: false, | |
| files: ["docs/routing/AUTO-COMBO.md", "docs/architecture/RESILIENCE_GUIDE.md"], | |
| }, | |
| { | |
| label: "OAuth providers count", | |
| actual: countFiles("src/lib/oauth/providers"), | |
| docKey: "OAuth providers", | |
| strict: false, | |
| files: ["docs/architecture/ARCHITECTURE.md"], | |
| }, | |
| { | |
| label: "A2A skills count", | |
| actual: countFiles("src/lib/a2a/skills"), | |
| docKey: "A2A skills", | |
| strict: false, | |
| files: ["docs/frameworks/A2A-SERVER.md"], | |
| }, | |
| { | |
| label: "Cloud agents count", | |
| actual: countFiles("src/lib/cloudAgent/agents"), | |
| docKey: "cloud agents", | |
| strict: false, | |
| files: ["docs/frameworks/CLOUD_AGENT.md", "docs/frameworks/AGENT_PROTOCOLS_GUIDE.md"], | |
| }, | |
| ]; | |
| } | |
| function main() { | |
| const checks = buildChecks(); | |
| const getContent = (relPath) => { | |
| const abs = path.join(ROOT, relPath); | |
| return fs.existsSync(abs) ? fs.readFileSync(abs, "utf8") : null; | |
| }; | |
| console.log("Docs counts sync report"); | |
| console.log("======================="); | |
| const { strict, soft, lines } = tallyDrift(checks, getContent); | |
| for (const l of lines) console.log(l); | |
| console.log(); | |
| if (strict > 0) { | |
| console.error( | |
| `✗ ${strict} STRICT drift(s) detected. ` + | |
| `Update the docs above to the real counts, or regenerate auto-generated sources ` + | |
| `(npm run gen:provider-reference).` | |
| ); | |
| process.exit(1); | |
| } | |
| if (soft > 0) { | |
| console.warn(`⚠ ${soft} potential (soft) drift(s) detected. Review the docs above.`); | |
| if (process.argv.includes("--strict")) process.exit(1); | |
| } else { | |
| console.log("✓ All checks pass."); | |
| } | |
| } | |
| const invokedDirectly = | |
| process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); | |
| if (invokedDirectly) main(); | |