import Database from "better-sqlite3"; import { execSync } from "node:child_process"; import { initSchema, clearAllData } from "./schema"; import { scanAllModules, scanFiles, scanSymbols, scanCalls, scanTechTags, scanFileImports, scanModuleDeps, } from "./scanner"; import { querySymbolRefs, querySymbolForwardCalls, queryModuleDeps, queryModuleReverseDeps, queryTechUsage, queryViolations, } from "./query"; const DB_PATH = "scripts/arch-scan/arch.db"; function getCommitHash(): string { try { return execSync("git rev-parse --short HEAD") .toString() .trim(); } catch { return "unknown"; } } export function runScan(): void { console.log("Starting architecture scan..."); const db = new Database(DB_PATH); db.pragma("journal_mode = WAL"); initSchema(db); clearAllData(db); console.log("Schema initialized."); scanAllModules(db, process.cwd()); console.log("Modules scanned."); // scanFiles 扫描 DB 中所有模块,只需调用一次 scanFiles(db, "src/modules"); console.log("Files scanned."); scanSymbols(db); console.log("Symbols scanned."); scanCalls(db); console.log("Calls scanned."); scanTechTags(db); console.log("Tech tags scanned."); scanFileImports(db); scanModuleDeps(db); console.log("Module dependencies scanned."); const now = new Date().toISOString(); const commit = getCommitHash(); const meta = db.prepare( "INSERT OR REPLACE INTO scan_meta (key, value) VALUES (?, ?)" ); meta.run("scanned_at", now); meta.run("commit_hash", commit); meta.run("scanner_version", "1.0.0"); const stats = { files: (db.prepare("SELECT count(*) as c FROM files").get() as { c: number }).c, symbols: (db.prepare("SELECT count(*) as c FROM symbols").get() as { c: number }).c, calls: (db.prepare("SELECT count(*) as c FROM calls").get() as { c: number }).c, }; meta.run("total_files", String(stats.files)); meta.run("total_symbols", String(stats.symbols)); meta.run("total_calls", String(stats.calls)); console.log( `Scan complete: ${stats.files} files, ${stats.symbols} symbols, ${stats.calls} calls` ); console.log(`Commit: ${commit}, Time: ${now}`); db.close(); } export function runQuery(args: string[]): void { const db = new Database(DB_PATH, { readonly: true }); const command = args[0]; switch (command) { case "ref": { const symbol = args[1]; if (!symbol) { console.error("Usage: arch:query ref [--forward] [--depth=N]"); process.exit(1); } const forward = args.includes("--forward"); const depthArg = args.find((a) => a.startsWith("--depth=")); const depth = depthArg ? parseInt(depthArg.split("=")[1], 10) : 10; if (forward) { const calls = querySymbolForwardCalls(db, symbol, depth); console.log(`▼ ${symbol}`); for (const call of calls) { const indent = " ".repeat(call.depth + 1); console.log( `${indent}└─▶ ${call.callee_name} (${call.callee_path}#L${call.callee_line})` ); } } else { const refs = querySymbolRefs(db, symbol, depth); console.log(`▼ ${symbol}`); for (const ref of refs) { const indent = " ".repeat(ref.depth + 1); console.log( `${indent}├─▼ ${ref.caller_name} (${ref.caller_path}#L${ref.caller_line})${ref.is_server_action ? " [Server Action]" : ""}` ); } } break; } case "module": { const moduleName = args[1]; if (!moduleName) { console.error("Usage: arch:query module [--reverse] [--depth=N]"); process.exit(1); } const reverse = args.includes("--reverse"); if (reverse) { const deps = queryModuleReverseDeps(db, moduleName); console.log(`▼ ${moduleName} (reverse deps)`); for (const dep of deps) { console.log(` └─◀ ${dep.source_module} [${dep.dep_type}]`); } } else { const deps = queryModuleDeps(db, moduleName); console.log(`▼ ${moduleName} (forward deps)`); for (const dep of deps) { console.log(` └─▶ ${dep.target_module} [${dep.dep_type}]`); } } break; } case "tech": { const tag = args[1]; if (!tag) { console.error("Usage: arch:query tech "); process.exit(1); } const usage = queryTechUsage(db, tag); console.log(`▼ tech: ${tag} (${usage.length} usages)`); for (const u of usage) { console.log(` └─ ${u.symbol_name} (${u.file_path})`); } break; } case "violations": { const v = queryViolations(db); console.log("▼ Architecture Violations"); console.log(` Long files (>800 lines): ${v.long_files.length}`); for (const f of v.long_files) { console.log(` └─ ${f.path}: ${f.lines} lines`); } console.log( ` Server Actions without requirePermission: ${v.server_actions_without_permission.length}` ); for (const sa of v.server_actions_without_permission) { console.log(` └─ ${sa.name} (${sa.path})`); } break; } case "sql": { const sql = args.slice(1).join(" "); if (!sql) { console.error('Usage: arch:query sql ""'); process.exit(1); } const rows = db.prepare(sql).all(); console.log(JSON.stringify(rows, null, 2)); break; } case "repl": { console.log("Entering SQLite REPL. Type .exit to quit."); console.log(`DB: ${DB_PATH}`); console.log( 'Tip: use "npm run arch:query -- sql \\"\\"" for quick queries instead.' ); break; } default: console.error( "Unknown command. Available: ref, module, tech, violations, sql, repl" ); process.exit(1); } db.close(); }