#!/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)}`);