deviant/scripts/scan-code.mjs

103 lines
3.4 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* scan-code.mjs — produces wiki/code-map.md
*
* Purpose: cheap, deterministic index of the codebase so future Claude
* sessions can locate symbols without a directory walk. Per CLAUDE.md:
* "Create scripts to navigate source code and reduce claude processing
* time and token burn."
*
* Outputs a single markdown file with:
* - Tree of src/, contracts/, test/, scripts/, wiki/
* - Per-file: exported symbols (TS), pragma + contract names (CashScript),
* line count, first doc-comment line.
*
* Intentionally zero external deps — runs on plain Node.
*/
import { readdirSync, readFileSync, statSync, writeFileSync, existsSync } from 'node:fs';
import { join, relative, extname } from 'node:path';
const ROOT = join(import.meta.dirname, '..');
const TARGETS = ['src', 'contracts', 'test', 'scripts', 'wiki'];
const OUT = join(ROOT, 'wiki', 'code-map.md');
const IGNORE = new Set(['node_modules', 'dist', '.git', 'coverage', '.regtest-data']);
function walk(dir, out = []) {
for (const entry of readdirSync(dir)) {
if (IGNORE.has(entry)) continue;
const full = join(dir, entry);
const st = statSync(full);
if (st.isDirectory()) walk(full, out);
else out.push(full);
}
return out;
}
function describe(file) {
const ext = extname(file);
const src = readFileSync(file, 'utf8');
const lines = src.split('\n');
const lineCount = lines.length;
const summary = { lineCount, symbols: [], doc: '' };
// First non-empty non-shebang line containing a comment marker
for (const line of lines.slice(0, 20)) {
const t = line.trim();
if (!t || t.startsWith('#!')) continue;
if (t.startsWith('//') || t.startsWith('/*') || t.startsWith('*') || t.startsWith('pragma')) {
summary.doc = t.replace(/^[/*\s]+/, '').slice(0, 120);
break;
}
}
if (ext === '.ts' || ext === '.mjs' || ext === '.js') {
const reExport = /^\s*export\s+(?:async\s+)?(?:function|class|const|let|interface|type|enum)\s+([A-Za-z0-9_]+)/gm;
let m;
while ((m = reExport.exec(src)) !== null) summary.symbols.push(m[1]);
}
if (ext === '.cash') {
const reContract = /^\s*contract\s+([A-Za-z0-9_]+)\s*\(/gm;
const reFn = /^\s*function\s+([A-Za-z0-9_]+)\s*\(/gm;
let m;
while ((m = reContract.exec(src)) !== null) summary.symbols.push(`contract:${m[1]}`);
while ((m = reFn.exec(src)) !== null) summary.symbols.push(`fn:${m[1]}`);
}
return summary;
}
const sections = [];
for (const target of TARGETS) {
const targetPath = join(ROOT, target);
if (!existsSync(targetPath)) continue;
const files = walk(targetPath)
.filter((f) => /\.(ts|mjs|js|cash|md)$/.test(f))
.sort();
if (files.length === 0) continue;
const rows = [];
for (const f of files) {
const rel = relative(ROOT, f).replaceAll('\\', '/');
const { lineCount, symbols, doc } = describe(f);
const sym = symbols.length ? symbols.join(', ') : '—';
rows.push(`| \`${rel}\` | ${lineCount} | ${sym} | ${doc || '—'} |`);
}
sections.push(
`### ${target}/\n\n| File | Lines | Exports | First doc line |\n|---|---|---|---|\n${rows.join('\n')}\n`
);
}
const stamp = new Date().toISOString();
const out = `# Code Map
_Auto-generated by \`scripts/scan-code.mjs\` at ${stamp}_
_Do not edit by hand. Regenerate with \`npm run scan\`._
${sections.join('\n')}
`;
writeFileSync(OUT, out);
console.log(`Wrote ${relative(ROOT, OUT)}`);