/** * scripts/generate-docs/lib/stats.ts * Counts the moving parts that should appear in the stats bar on index.html. * Numbers are derived from the filesystem so they can never go stale. */ import { readFileSync } from 'node:fs'; import { sync as globSync } from 'glob'; import { join } from 'node:path'; import { parseAllSkills } from './skill-parser'; const REPO_ROOT = join(__dirname, '..', '..', '..'); export interface Stats { skills: number; baSkills: number; agents: number; hooks: number; cliCommands: number; /** * Audit-rule ranges per code prefix, rendered as `PREFIX-001..0NN`. * Derived by scanning every BA SKILL.md body for `PREFIX-NNN` occurrences, * so the documented ranges can never drift from the shipped rules. * Usage in docs-src markdown / templates: `{{stats.rules.UC}}` → `UC-001..021`. */ rules: Record; } function count(glob: string): number { return globSync(glob.replace(/\\/g, '/'), { cwd: REPO_ROOT }).length; } function collectRuleRanges(): Record { const files = globSync('templates/skills/business-analyse/**/SKILL.md', { cwd: REPO_ROOT, absolute: true, ignore: '**/node_modules/**', }); const maxByPrefix = new Map(); const pattern = /\b([A-Z]{2,6})-(\d{3})\b/g; for (const file of files) { const content = readFileSync(file, 'utf-8'); for (const m of content.matchAll(pattern)) { const prefix = m[1]; const num = parseInt(m[2], 10); if (num > (maxByPrefix.get(prefix) ?? 0)) maxByPrefix.set(prefix, num); } } const out: Record = {}; for (const [prefix, max] of maxByPrefix) { out[prefix] = `${prefix}-001..${String(max).padStart(3, '0')}`; } return out; } export function collectStats(): Stats { const skills = parseAllSkills(); return { skills: skills.length, baSkills: skills.filter((s) => s.phase === 'business-analyse').length, agents: count('templates/agents/*.md'), hooks: count('templates/hooks/*.{sh,js,mjs,cjs}'), // CLI commands = TypeScript files in src/commands/ excluding the barrel index.ts cliCommands: globSync('src/commands/*.ts', { cwd: REPO_ROOT }) .filter((f) => !/[\\/]index\.ts$/.test(f)).length, rules: collectRuleRanges(), }; }