#!/usr/bin/env node // 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();