UNPKG

scai

Version:

> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ❤️.

185 lines (184 loc) 7.48 kB
import Database from "better-sqlite3"; import path from "path"; import os from "os"; import fs from "fs"; import { execSync } from "child_process"; import { Config } from "../config.js"; /* ───────────────────────── bootstrap ───────────────────────── */ const cfg = Config.getRaw(); const repoKey = cfg.activeRepo; if (!repoKey) { console.error("❌ No active repo found. Use `scai set-index`."); process.exit(1); } const repoName = path.basename(repoKey); const scaiRepoRoot = path.join(os.homedir(), ".scai", "repos", repoName); const dbPath = path.join(scaiRepoRoot, "db.sqlite"); if (!fs.existsSync(dbPath)) { console.error(`❌ DB not found: ${dbPath}`); process.exit(1); } const db = new Database(dbPath); /* ───────────────────────── helpers ───────────────────────── */ function tableCount(table) { return db.prepare(`SELECT COUNT(*) AS c FROM ${table}`).get().c; } function nonEmptyCount(table, col) { return db.prepare(`SELECT COUNT(*) AS c FROM ${table} WHERE ${col} IS NOT NULL AND ${col} != ''`).get().c; } function header(title) { console.log(`\n${title}`); } /* ───────────────────────── files ───────────────────────── */ header("🗂 files"); const totalFiles = tableCount("files"); const filesWithContent = nonEmptyCount("files", "content_text"); console.log(`📊 total files: ${totalFiles}`); console.log(`📄 files with content: ${filesWithContent}`); header("⚙️ processing_status"); const statuses = db.prepare(` SELECT processing_status, COUNT(*) AS count FROM files GROUP BY processing_status ORDER BY count DESC `).all(); statuses.forEach(s => console.log(` ${s.processing_status ?? "NULL"}: ${s.count}`)); /* ───────────────────────── FTS ───────────────────────── */ header("🔍 files_fts"); const ftsExists = db .prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name='files_fts'`) .get(); if (!ftsExists) { console.log("❌ files_fts missing — search is broken"); } else { const cols = db.prepare(`PRAGMA table_info(files_fts)`).all(); const hasContentText = cols.some(c => c.name === "content_text"); if (!hasContentText) { console.log("❌ files_fts.content_text missing"); } else { const indexed = nonEmptyCount("files_fts", "content_text"); console.log(`📦 indexed rows with content_text: ${indexed}`); const sample = db.prepare(` SELECT filename, substr(content_text,1,80) AS preview FROM files_fts WHERE files_fts MATCH 'function' LIMIT 3 `).all(); if (sample.length === 0) { console.log("⚠️ no FTS hits for 'function'"); } else { console.log("✅ FTS sample:"); sample.forEach(r => console.log(` ${r.filename} | "${r.preview}"`)); } } } /* ───────────────────────── folder capsules ───────────────────────── */ header("📁 folder_capsules"); console.log(`📊 total capsules: ${tableCount("folder_capsules")}`); const emptyCapsules = db.prepare(` SELECT COUNT(*) AS c FROM folder_capsules WHERE capsule_json IS NULL OR capsule_json = '' `).get(); if (emptyCapsules.c > 0) { console.log(`⚠️ ${emptyCapsules.c} capsules have empty JSON`); } const sampleCaps = db.prepare(` SELECT path, depth, confidence, source_file_count, capsule_json FROM folder_capsules ORDER BY RANDOM() LIMIT 5 `).all(); sampleCaps.forEach(c => { let parsed = null; try { parsed = JSON.parse(c.capsule_json); } catch { } console.log(` ${c.path}`); console.log(` depth=${c.depth} confidence=${c.confidence} files=${c.source_file_count}`); if (parsed) { console.log(` roles=${parsed.roles?.length ?? 0} ` + `concerns=${parsed.concerns?.length ?? 0} ` + `keyFiles=${parsed.keyFiles?.length ?? 0}`); } }); /* ───────────────────────── functions ───────────────────────── */ header("🧑‍💻 functions"); console.log(`📊 total functions: ${tableCount("functions")}`); const funcSamples = db.prepare(` SELECT id, name, file_id, substr(content,1,60) AS preview FROM functions ORDER BY RANDOM() LIMIT 5 `).all(); funcSamples.forEach(f => console.log(` [${f.id}] ${f.name} (file ${f.file_id}) "${f.preview}"`)); /* ───────────────────────── graph (KG) ───────────────────────── */ header("🏷 graph_classes"); console.log(`📊 total classes: ${tableCount("graph_classes")}`); header("🔗 graph_edges"); console.log(`📊 total edges: ${tableCount("graph_edges")}`); const danglingFuncs = db.prepare(` SELECT e.id FROM graph_edges e WHERE e.source_type='function' AND NOT EXISTS (SELECT 1 FROM functions f WHERE f.unique_id = e.source_unique_id) UNION SELECT e.id FROM graph_edges e WHERE e.target_type='function' AND NOT EXISTS (SELECT 1 FROM functions f WHERE f.unique_id = e.target_unique_id) LIMIT 10 `).all(); if (danglingFuncs.length === 0) { console.log("✅ no dangling function edges"); } else { console.log(`❌ dangling function edges: ${danglingFuncs.length}`); } /* ───────────────────────── Graphviz ───────────────────────── */ header("🧩 graphviz"); try { const edges = db.prepare(` SELECT source_type, source_unique_id, target_type, target_unique_id, relation FROM graph_edges LIMIT 300 `).all(); if (edges.length === 0) { console.log("⚠️ no edges — skipping export"); } else { const lines = [ "digraph G {", " rankdir=LR;", ' node [shape=box, style=filled, color="#eaeaea"];' ]; for (const e of edges) { const s = `${e.source_type}_${e.source_unique_id}`.replace(/[^a-zA-Z0-9_]/g, "_"); const t = `${e.target_type}_${e.target_unique_id}`.replace(/[^a-zA-Z0-9_]/g, "_"); const label = (e.relation ?? "").replace(/"/g, "'"); lines.push(` ${s} -> ${t} [label="${label}"];`); } lines.push("}"); const outDir = path.join(scaiRepoRoot, "graphs"); fs.mkdirSync(outDir, { recursive: true }); const dot = path.join(outDir, "graph-overview.dot"); const png = path.join(outDir, "graph-overview.png"); fs.writeFileSync(dot, lines.join("\n")); console.log(`✅ dot written: ${dot}`); try { execSync(`dot -Tpng "${dot}" -o "${png}"`); console.log(`✅ png written: ${png}`); } catch { console.log("⚠️ graphviz not installed — png skipped"); } } } catch (err) { console.error("❌ graphviz export failed", err); } console.log("\n✅ DB check complete\n");