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
JavaScript
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");