scai
Version:
> **A local-first AI CLI for understanding, querying, and iterating on large codebases.** > **100% local • No token costs • No cloud • No prompt injection • Private by design**
286 lines (285 loc) • 11.4 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}`);
}
/* ───────────────────────── global / project / task sanity ───────────────────────── */
header("🌐 global_state / projects / tasks sanity");
// --- global_state ---
const globalExists = db
.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name='global_state'`)
.get();
if (!globalExists) {
console.log("❌ global_state table missing — run initSchema");
}
else {
const globalRow = db.prepare(`SELECT * FROM global_state WHERE id=1`).get();
if (!globalRow) {
console.log("⚠️ global_state row missing (id=1) — should be auto-created");
}
else {
console.log("✅ global_state row exists");
}
}
// --- projects ---
const projectsExists = db
.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name='projects'`)
.get();
if (!projectsExists) {
console.log("❌ projects table missing — run initSchema");
}
else {
const totalProjects = tableCount("projects");
if (totalProjects === 0) {
console.log("⚠️ no projects found — should be at least one default project");
}
else {
console.log(`✅ total projects: ${totalProjects}`);
}
}
// --- tasks ---
header("📝 tasks");
const tasksExists = db
.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name='tasks'`)
.get();
if (!tasksExists) {
console.log("❌ tasks table missing — you need to run initSchema with tasks");
}
else {
const totalTasks = tableCount("tasks");
const tasksWithQuery = nonEmptyCount("tasks", "initial_query");
console.log(`📊 total tasks: ${totalTasks}`);
console.log(`✏️ tasks with initial_query: ${tasksWithQuery}`);
const sampleTasks = db.prepare(`
SELECT id, initial_query, status, created_at
FROM tasks
ORDER BY created_at DESC
LIMIT 3
`).all();
if (sampleTasks.length === 0) {
console.log("⚠️ no tasks yet");
}
else {
console.log("✅ sample tasks:");
sampleTasks.forEach(t => console.log(` [${t.id}] "${t.initial_query}" (status=${t.status}, created=${t.created_at})`));
}
}
/* ───────────────────────── task_steps sanity ───────────────────────── */
header("🦾 task_steps");
const taskStepsExists = db
.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name='task_steps'`)
.get();
if (!taskStepsExists) {
console.log("❌ task_steps table missing — you need to run initSchema with task_steps");
}
else {
const totalTaskSteps = tableCount("task_steps");
const pendingSteps = nonEmptyCount("task_steps", "status"); // all steps have status
console.log(`📊 total task_steps: ${totalTaskSteps}`);
console.log(`📝 task_steps with status: ${pendingSteps}`);
const sampleSteps = db.prepare(`
SELECT id, task_id, file_path, status, step_index, created_at
FROM task_steps
ORDER BY created_at DESC
LIMIT 3
`).all();
if (sampleSteps.length === 0) {
console.log("⚠️ no task_steps yet");
}
else {
console.log("✅ sample task_steps:");
sampleSteps.forEach(s => console.log(` [${s.id}] task ${s.task_id} - file "${s.file_path}" (status=${s.status}, index=${s.step_index}, created=${s.created_at})`));
}
// Check for steps referencing missing tasks
const danglingSteps = db.prepare(`
SELECT ts.id, ts.task_id
FROM task_steps ts
LEFT JOIN tasks t ON ts.task_id = t.id
WHERE t.id IS NULL
`).all();
if (danglingSteps.length > 0) {
console.log(`❌ ${danglingSteps.length} task_steps reference missing tasks`);
}
else {
console.log("✅ all task_steps reference valid tasks");
}
}
/* ───────────────────────── 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");