scai 0.1.108 → 0.1.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/CHANGELOG.md CHANGED
@@ -166,4 +166,27 @@ Type handling with the module pipeline
166
166
 
167
167
  ## 2025-09-01
168
168
 
169
- * Improve handling of GitHub repository URLs and paths by extracting the owner and name separately
169
+ * Improve handling of GitHub repository URLs and paths by extracting the owner and name separately
170
+
171
+ ## 2025-09-02
172
+
173
+ • Added test configuration for project and generated tests
174
+ • Add runTestsModule and repairTestsModule for testing pipeline
175
+
176
+ ## 2025-09-05
177
+
178
+ • Enable execution of files as executable files in the scripts
179
+ • Remove context failure if models not installed
180
+ • Add ability to set global model
181
+
182
+ ## 2025-09-08
183
+
184
+ ### Requires DB reset ('scai db reset' followed by 'scai index start')
185
+
186
+ 1. Improved daemon batch processing by skipping missing files, classifying unknown file types, and persisting entities/tags in the database.
187
+ 2. Invoke kgModule in daemonBatch to build knowledge graphs after indexing.
188
+ 3. Improved data modeling and extraction logic for functions and classes in TypeScript files.
189
+ 4. Updated Edge/Table schema for better query performance.
190
+ 5. Update package-lock.json to caniuse-lite@1.0.30001741.
191
+ 6. Enable execution of as an executable file in the scripts.
192
+ 7. Remove context failure if models not installed. Add ability to set global model.
@@ -0,0 +1,10 @@
1
+ import { SCAI_HOME } from "../constants"; // example constant
2
+ // cli/src/__tests__/example.test.ts
3
+ describe("CLI src basic test", () => {
4
+ it("should pass a simple truthy test", () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ it("should import a constant from cli/src/constants.ts", () => {
8
+ expect(typeof SCAI_HOME).not.toBe("undefined");
9
+ });
10
+ });
@@ -18,8 +18,20 @@ export class Agent {
18
18
  // Resolve modules (with before/after dependencies)
19
19
  const modules = this.resolveModules(this.goals);
20
20
  console.log(chalk.green("📋 Modules to run:"), modules.map((m) => m.name).join(" → "));
21
- // Read file content (optional, could be used by modules in workflow)
22
- await fs.readFile(filepath, "utf-8");
21
+ try {
22
+ // Check that the file exists before trying to read it
23
+ await fs.access(filepath);
24
+ // Read file content (optional, could be used by modules in workflow)
25
+ await fs.readFile(filepath, "utf-8");
26
+ }
27
+ catch (err) {
28
+ if (err.code === "ENOENT") {
29
+ console.error(chalk.redBright("❌ Error:"), `File not found: ${chalk.yellow(filepath)}`);
30
+ console.error(`Make sure the path is correct. (cwd: ${chalk.gray(process.cwd())})`);
31
+ process.exit(1);
32
+ }
33
+ throw err; // rethrow for unexpected errors
34
+ }
23
35
  // Delegate everything to handleAgentRun (like CLI commands do)
24
36
  await handleAgentRun(filepath, modules);
25
37
  console.log(chalk.green("✅ Agent finished!"));
@@ -76,6 +76,11 @@ export async function handleAgentRun(filepath, modules) {
76
76
  baseChunks.length = 0;
77
77
  baseChunks.push(...reset);
78
78
  break;
79
+ case 'skip':
80
+ console.log(chalk.gray(`⏭️ Skipped writing for module ${mod.name}`));
81
+ // don’t touch files, but keep chunks flowing
82
+ workingChunks = processed;
83
+ break;
79
84
  default:
80
85
  console.log(chalk.yellow(`⚠️ Unknown mode; skipping write`));
81
86
  // still move pipeline forward with processed
@@ -23,9 +23,11 @@ export async function startDaemon() {
23
23
  const __filename = fileURLToPath(import.meta.url);
24
24
  const __dirname = path.dirname(__filename);
25
25
  const daemonWorkerPath = path.join(__dirname, '../daemon/daemonWorker.js');
26
+ const out = fsSync.openSync(LOG_PATH, 'a');
27
+ const err = fsSync.openSync(LOG_PATH, 'a');
26
28
  const child = spawn(process.execPath, [daemonWorkerPath], {
27
29
  detached: true,
28
- stdio: ['ignore', 'ignore', 'ignore'],
30
+ stdio: ['ignore', out, err], // stdout/stderr -> log file
29
31
  env: {
30
32
  ...process.env,
31
33
  BACKGROUND_MODE: 'true',
package/dist/config.js CHANGED
@@ -6,7 +6,7 @@ import { normalizePath } from './utils/contentUtils.js';
6
6
  import chalk from 'chalk';
7
7
  import { getHashedRepoKey } from './utils/repoKey.js';
8
8
  const defaultConfig = {
9
- model: 'codellama:13b',
9
+ model: 'llama3:8b',
10
10
  contextLength: 4096,
11
11
  language: 'ts',
12
12
  indexDir: '',
@@ -55,18 +55,23 @@ export const Config = {
55
55
  const repoCfg = cfg.repos?.[cfg.activeRepo ?? ''];
56
56
  return repoCfg?.model || cfg.model;
57
57
  },
58
- setModel(model) {
58
+ setModel(model, scope = 'repo') {
59
59
  const cfg = readConfig();
60
- const active = cfg.activeRepo;
61
- if (active) {
60
+ if (scope === 'repo') {
61
+ const active = cfg.activeRepo;
62
+ if (!active) {
63
+ console.error("❌ No active repo to set model for.");
64
+ return;
65
+ }
62
66
  cfg.repos[active] = { ...cfg.repos[active], model };
63
- writeConfig(cfg);
64
- console.log(`📦 Model set to: ${model}`);
67
+ console.log(`📦 Model set for repo '${active}': ${model}`);
65
68
  }
66
69
  else {
67
- writeConfig({ model });
68
- console.log(`📦 Default model set to: ${model}`);
70
+ // Set global default model
71
+ cfg.model = model;
72
+ console.log(`📦 Global default model set to: ${model}`);
69
73
  }
74
+ writeConfig(cfg);
70
75
  },
71
76
  getLanguage() {
72
77
  const cfg = readConfig();
package/dist/context.js CHANGED
@@ -5,12 +5,25 @@ import { getHashedRepoKey } from "./utils/repoKey.js";
5
5
  import { getDbForRepo, getDbPathForRepo } from "./db/client.js";
6
6
  import fs from "fs";
7
7
  import chalk from "chalk";
8
+ import { execSync } from "child_process";
9
+ function modelExists(model) {
10
+ try {
11
+ const output = execSync("ollama list", { encoding: "utf-8" });
12
+ return output
13
+ .split("\n")
14
+ .map(line => line.trim())
15
+ .filter(Boolean)
16
+ .some(line => line.toLowerCase().startsWith(model.toLowerCase() + " ") || line.toLowerCase() === model.toLowerCase());
17
+ }
18
+ catch (err) {
19
+ console.error(chalk.red("❌ Failed to check models with `ollama list`"));
20
+ return false;
21
+ }
22
+ }
8
23
  export async function updateContext() {
9
24
  const cwd = normalizePath(process.cwd());
10
25
  const cfg = readConfig();
11
- // 🔑 Find repoKey by matching indexDir to cwd
12
26
  let repoKey = Object.keys(cfg.repos || {}).find((key) => normalizePath(cfg.repos[key]?.indexDir || "") === cwd);
13
- // Initialize new repo config if not found
14
27
  let isNewRepo = false;
15
28
  if (!repoKey) {
16
29
  repoKey = getHashedRepoKey(cwd);
@@ -19,28 +32,23 @@ export async function updateContext() {
19
32
  cfg.repos[repoKey].indexDir = cwd;
20
33
  isNewRepo = true;
21
34
  }
22
- // Check if active repo has changed
23
35
  const activeRepoChanged = cfg.activeRepo !== repoKey;
24
- // Always set this as active repo
25
36
  cfg.activeRepo = repoKey;
26
37
  writeConfig(cfg);
27
38
  const repoCfg = cfg.repos[repoKey];
28
39
  let ok = true;
29
- // Only log detailed info if new repo or active repo changed
30
40
  if (isNewRepo || activeRepoChanged) {
31
41
  console.log(chalk.yellow("\n🔁 Updating context...\n"));
32
42
  console.log(`✅ Active repo: ${chalk.green(repoKey)}`);
33
43
  console.log(`✅ Index dir: ${chalk.cyan(repoCfg.indexDir || cwd)}`);
34
44
  }
35
- // GitHub token is optional
36
45
  const token = repoCfg.githubToken || cfg.githubToken;
37
46
  if (!token) {
38
- console.log(`ℹ️ No GitHub token found. You can set one with the: ${chalk.bold(chalk.bgGreen("scai auth set"))} command`);
47
+ console.log(`ℹ️ No GitHub token found. You can set one with: ${chalk.bold(chalk.bgGreen("scai auth set"))}`);
39
48
  }
40
49
  else if (isNewRepo || activeRepoChanged) {
41
50
  console.log(`✅ GitHub token present`);
42
51
  }
43
- // Ensure DB exists
44
52
  const dbPath = getDbPathForRepo();
45
53
  if (!fs.existsSync(dbPath)) {
46
54
  console.log(chalk.yellow(`📦 Initializing DB at ${dbPath}`));
@@ -48,13 +56,31 @@ export async function updateContext() {
48
56
  getDbForRepo();
49
57
  }
50
58
  catch {
51
- ok = false; // DB init failed
59
+ ok = false;
52
60
  }
53
61
  }
54
62
  else if (isNewRepo || activeRepoChanged) {
55
63
  console.log(chalk.green("✅ Database present"));
56
64
  }
57
- // Final context status
65
+ // 🧠 Model check
66
+ const model = cfg.model;
67
+ if (!model) {
68
+ console.log(chalk.red("❌ No model configured.") +
69
+ "\n➡️ Set one with: " +
70
+ chalk.bold(chalk.bgGreen("scai config set-model <model>")));
71
+ ok = false;
72
+ }
73
+ else if (!modelExists(model)) {
74
+ console.log(chalk.red(`❌ Model '${model}' not installed in Ollama.`) +
75
+ "\n➡️ Install with: " +
76
+ chalk.bold(chalk.yellow(`ollama pull ${model}`)) +
77
+ " or choose another with: " +
78
+ chalk.bold(chalk.yellow("scai config set-model <model>")));
79
+ ok = false;
80
+ }
81
+ else {
82
+ console.log(chalk.green(`✅ Model '${model}' available`));
83
+ }
58
84
  if (ok) {
59
85
  console.log(chalk.bold.green("\n✅ Context OK\n"));
60
86
  }
@@ -1,4 +1,4 @@
1
- import { indexFunctionsForFile } from '../db/functionIndex.js';
1
+ import { indexCodeForFile } from '../db/functionIndex.js';
2
2
  import fs from 'fs/promises';
3
3
  import fsSync from 'fs';
4
4
  import { generateEmbedding } from '../lib/generateEmbedding.js';
@@ -8,6 +8,7 @@ import { summaryModule } from '../pipeline/modules/summaryModule.js';
8
8
  import { classifyFile } from '../fileRules/classifyFile.js';
9
9
  import { getDbForRepo, getDbPathForRepo } from '../db/client.js';
10
10
  import { markFileAsSkippedByPath, selectUnprocessedFiles, updateFileWithSummaryAndEmbedding, } from '../db/sqlTemplates.js';
11
+ import { kgModule } from '../pipeline/modules/kgModule.js';
11
12
  const MAX_FILES_PER_BATCH = 5;
12
13
  /**
13
14
  * Acquires a lock on the database to ensure that only one daemon batch
@@ -32,7 +33,6 @@ async function lockDb() {
32
33
  */
33
34
  export async function runDaemonBatch() {
34
35
  log('🟡 Starting daemon batch...');
35
- // Selects up to MAX_FILES_PER_BATCH files that haven't been processed yet
36
36
  const db = getDbForRepo();
37
37
  const rows = db.prepare(selectUnprocessedFiles).all(MAX_FILES_PER_BATCH);
38
38
  if (rows.length === 0) {
@@ -42,13 +42,11 @@ export async function runDaemonBatch() {
42
42
  const release = await lockDb();
43
43
  for (const row of rows) {
44
44
  log(`📂 Processing file: ${row.path}`);
45
- // Skip if file is missing from the file system
46
45
  if (!fsSync.existsSync(row.path)) {
47
46
  log(`⚠️ Skipped missing file: ${row.path}`);
48
47
  db.prepare(markFileAsSkippedByPath).run({ path: row.path });
49
48
  continue;
50
49
  }
51
- // Skip if file is classified as something we don't process
52
50
  const classification = classifyFile(row.path);
53
51
  if (classification !== 'valid') {
54
52
  log(`⏭️ Skipping (${classification}): ${row.path}`);
@@ -57,24 +55,20 @@ export async function runDaemonBatch() {
57
55
  }
58
56
  try {
59
57
  const content = await fs.readFile(row.path, 'utf-8');
60
- // Determine whether the file needs to be re-summarized
61
58
  const needsResummary = !row.summary ||
62
59
  !row.indexed_at ||
63
60
  (row.last_modified && new Date(row.last_modified) > new Date(row.indexed_at));
64
61
  if (needsResummary) {
65
62
  log(`📝 Generating summary for ${row.path}...`);
66
- // Generate a summary using the summary pipeline
67
63
  const summaryResult = await summaryModule.run({ content, filepath: row.path });
68
64
  const summary = summaryResult?.summary?.trim() || null;
69
65
  let embedding = null;
70
- // Generate an embedding from the summary (if present)
71
66
  if (summary) {
72
67
  const vector = await generateEmbedding(summary);
73
68
  if (vector) {
74
69
  embedding = JSON.stringify(vector);
75
70
  }
76
71
  }
77
- // Update the file record with the new summary and embedding
78
72
  db.prepare(updateFileWithSummaryAndEmbedding).run({
79
73
  summary,
80
74
  embedding,
@@ -85,19 +79,79 @@ export async function runDaemonBatch() {
85
79
  else {
86
80
  log(`⚡ Skipped summary (up-to-date) for ${row.path}`);
87
81
  }
88
- // Extract top-level functions from the file and update the DB
89
- const extracted = await indexFunctionsForFile(row.path, row.id);
90
- if (extracted) {
91
- log(`✅ Function extraction complete for ${row.path}\n`);
82
+ const success = await indexCodeForFile(row.path, row.id);
83
+ if (success) {
84
+ log(`✅ Indexed code for ${row.path}`);
85
+ try {
86
+ log(`🔗 Building Knowledge Graph for ${row.path}...`);
87
+ const kgInput = {
88
+ fileId: row.id,
89
+ filepath: row.path,
90
+ summary: row.summary || undefined,
91
+ };
92
+ const kgResult = await kgModule.run(kgInput, content);
93
+ log(`✅ Knowledge Graph built for ${row.path}`);
94
+ log(`Entities: ${kgResult.entities.length}, Edges: ${kgResult.edges.length}`);
95
+ // Persist KG entities + tags only if there are any
96
+ if (kgResult.entities.length > 0) {
97
+ const insertTag = db.prepare(`
98
+ INSERT OR IGNORE INTO tags_master (name) VALUES (:name)
99
+ `);
100
+ const getTagId = db.prepare(`
101
+ SELECT id FROM tags_master WHERE name = :name
102
+ `);
103
+ const insertEntityTag = db.prepare(`
104
+ INSERT OR IGNORE INTO entity_tags (entity_type, entity_id, tag_id)
105
+ VALUES (:entity_type, :entity_id, :tag_id)
106
+ `);
107
+ for (const entity of kgResult.entities) {
108
+ // Skip entity if type or tags are missing
109
+ if (!entity.type || !Array.isArray(entity.tags) || entity.tags.length === 0) {
110
+ console.warn(`⚠ Skipping entity due to missing type or tags:`, entity);
111
+ continue;
112
+ }
113
+ for (const tag of entity.tags) {
114
+ // Skip empty or invalid tags
115
+ if (!tag || typeof tag !== 'string') {
116
+ console.warn(`⚠ Skipping invalid tag for entity ${entity.type}:`, tag);
117
+ continue;
118
+ }
119
+ try {
120
+ // ✅ Use :name in SQL and plain key in object
121
+ insertTag.run({ name: tag });
122
+ const tagRow = getTagId.get({ name: tag });
123
+ if (!tagRow) {
124
+ console.warn(`⚠ Could not find tag ID for: ${tag}`);
125
+ continue;
126
+ }
127
+ insertEntityTag.run({
128
+ entity_type: entity.type,
129
+ entity_id: row.id,
130
+ tag_id: tagRow.id,
131
+ });
132
+ }
133
+ catch (err) {
134
+ console.error(`❌ Failed to persist entity/tag:`, { entity, tag, error: err });
135
+ }
136
+ }
137
+ }
138
+ log(`✅ Persisted entities + tags for ${row.path}`);
139
+ }
140
+ else {
141
+ log(`⚠️ No entities found for ${row.path}, skipping DB inserts`);
142
+ }
143
+ }
144
+ catch (kgErr) {
145
+ log(`❌ KG build failed for ${row.path}: ${kgErr instanceof Error ? kgErr.message : String(kgErr)}`);
146
+ }
92
147
  }
93
148
  else {
94
- log(`ℹ️ No functions extracted for ${row.path}\n`);
149
+ log(`ℹ️ No code elements extracted for ${row.path}`);
95
150
  }
96
151
  }
97
152
  catch (err) {
98
153
  log(`❌ Failed: ${row.path}: ${err instanceof Error ? err.message : String(err)}\n`);
99
154
  }
100
- // Add a small delay to throttle processing
101
155
  await new Promise(resolve => setTimeout(resolve, 200));
102
156
  }
103
157
  await release();
@@ -31,9 +31,26 @@ export async function daemonWorker() {
31
31
  while (true) {
32
32
  try {
33
33
  log('🔄 Running daemon batch...');
34
- const didWork = await runDaemonBatch();
34
+ // Wrap the batch in debug
35
+ let didWork = false;
36
+ try {
37
+ log('🔹 Running runDaemonBatch()...');
38
+ didWork = await runDaemonBatch();
39
+ log('✅ runDaemonBatch() completed successfully');
40
+ }
41
+ catch (batchErr) {
42
+ log('🔥 Error inside runDaemonBatch():', batchErr);
43
+ }
35
44
  if (!didWork) {
36
- const queueEmpty = await isQueueEmpty();
45
+ let queueEmpty = false;
46
+ try {
47
+ log('🔹 Checking if queue is empty...');
48
+ queueEmpty = await isQueueEmpty();
49
+ log(`🔹 Queue empty status: ${queueEmpty}`);
50
+ }
51
+ catch (queueErr) {
52
+ log('🔥 Error checking queue status:', queueErr);
53
+ }
37
54
  if (queueEmpty) {
38
55
  log('🕊️ No work found. Idling...');
39
56
  await sleep(IDLE_SLEEP_MS * 3);
@@ -29,6 +29,7 @@ export async function extractFromJS(filePath, content, fileId) {
29
29
  locations: true,
30
30
  });
31
31
  const functions = [];
32
+ const classes = [];
32
33
  walkAncestor(ast, {
33
34
  FunctionDeclaration(node, ancestors) {
34
35
  const parent = ancestors[ancestors.length - 2];
@@ -60,31 +61,63 @@ export async function extractFromJS(filePath, content, fileId) {
60
61
  content: content.slice(node.start, node.end),
61
62
  });
62
63
  },
64
+ ClassDeclaration(node) {
65
+ const className = node.id?.name || `${path.basename(filePath)}:<anon-class>`;
66
+ classes.push({
67
+ name: className,
68
+ start_line: node.loc?.start.line ?? -1,
69
+ end_line: node.loc?.end.line ?? -1,
70
+ content: content.slice(node.start, node.end),
71
+ superClass: node.superClass?.name ?? null,
72
+ });
73
+ },
74
+ ClassExpression(node) {
75
+ const className = node.id?.name || `${path.basename(filePath)}:<anon-class>`;
76
+ classes.push({
77
+ name: className,
78
+ start_line: node.loc?.start.line ?? -1,
79
+ end_line: node.loc?.end.line ?? -1,
80
+ content: content.slice(node.start, node.end),
81
+ superClass: node.superClass?.name ?? null,
82
+ });
83
+ },
63
84
  });
64
- if (functions.length === 0) {
65
- log(`⚠️ No functions found in: ${filePath}`);
85
+ if (functions.length === 0 && classes.length === 0) {
86
+ log(`⚠️ No functions/classes found in: ${filePath}`);
66
87
  db.prepare(markFileAsSkippedTemplate).run({ id: fileId });
67
88
  return false;
68
89
  }
69
- log(`🔍 Found ${functions.length} functions in ${filePath}`);
90
+ log(`🔍 Found ${functions.length} functions and ${classes.length} classes in ${filePath}`);
91
+ // Insert functions
70
92
  for (const fn of functions) {
71
93
  const embedding = await generateEmbedding(fn.content);
72
- const result = db.prepare(`
94
+ const result = db
95
+ .prepare(`
73
96
  INSERT INTO functions (
74
97
  file_id, name, start_line, end_line, content, embedding, lang
75
98
  ) VALUES (
76
99
  @file_id, @name, @start_line, @end_line, @content, @embedding, @lang
77
100
  )
78
- `).run({
101
+ `)
102
+ .run({
79
103
  file_id: fileId,
80
104
  name: fn.name,
81
105
  start_line: fn.start_line,
82
106
  end_line: fn.end_line,
83
107
  content: fn.content,
84
108
  embedding: JSON.stringify(embedding),
85
- lang: 'js'
109
+ lang: 'js',
110
+ });
111
+ const functionId = result.lastInsertRowid;
112
+ // file → function edge
113
+ db.prepare(`INSERT INTO edges (source_type, source_id, target_type, target_id, relation)
114
+ VALUES (@source_type, @source_id, @target_type, @target_id, 'contains')`).run({
115
+ source_type: 'file',
116
+ source_id: fileId,
117
+ target_type: 'function',
118
+ target_id: functionId,
86
119
  });
87
- const callerId = result.lastInsertRowid;
120
+ // Walk inside function to find calls
88
121
  const fnAst = parse(fn.content, {
89
122
  ecmaVersion: 'latest',
90
123
  sourceType: 'module',
@@ -96,26 +129,73 @@ export async function extractFromJS(filePath, content, fileId) {
96
129
  if (node.callee?.type === 'Identifier' && node.callee.name) {
97
130
  calls.push({ calleeName: node.callee.name });
98
131
  }
99
- }
132
+ },
100
133
  });
101
134
  for (const call of calls) {
102
- db.prepare(`
103
- INSERT INTO function_calls (caller_id, callee_name)
104
- VALUES (@caller_id, @callee_name)
105
- `).run({
106
- caller_id: callerId,
107
- callee_name: call.calleeName
135
+ // Store name for later resolution
136
+ db.prepare(`INSERT INTO function_calls (caller_id, callee_name) VALUES (@caller_id, @callee_name)`).run({ caller_id: functionId, callee_name: call.calleeName });
137
+ // Optional unresolved edge
138
+ db.prepare(`INSERT INTO edges (source_type, source_id, target_type, target_id, relation)
139
+ VALUES (@source_type, @source_id, @target_type, @target_id, 'calls')`).run({
140
+ source_type: 'function',
141
+ source_id: functionId,
142
+ target_type: 'function',
143
+ target_id: 0, // unresolved callee
108
144
  });
109
145
  }
110
146
  log(`📌 Indexed function: ${fn.name} with ${calls.length} calls`);
111
147
  }
148
+ // Insert classes
149
+ for (const cls of classes) {
150
+ const embedding = await generateEmbedding(cls.content);
151
+ const result = db
152
+ .prepare(`
153
+ INSERT INTO classes (
154
+ file_id, name, start_line, end_line, content, embedding, lang
155
+ ) VALUES (
156
+ @file_id, @name, @start_line, @end_line, @content, @embedding, @lang
157
+ )
158
+ `)
159
+ .run({
160
+ file_id: fileId,
161
+ name: cls.name,
162
+ start_line: cls.start_line,
163
+ end_line: cls.end_line,
164
+ content: cls.content,
165
+ embedding: JSON.stringify(embedding),
166
+ lang: 'js',
167
+ });
168
+ const classId = result.lastInsertRowid;
169
+ // file → class edge
170
+ db.prepare(`INSERT INTO edges (source_type, source_id, target_type, target_id, relation)
171
+ VALUES (@source_type, @source_id, @target_type, @target_id, 'contains')`).run({
172
+ source_type: 'file',
173
+ source_id: fileId,
174
+ target_type: 'class',
175
+ target_id: classId,
176
+ });
177
+ // superclass → store unresolved reference
178
+ if (cls.superClass) {
179
+ db.prepare(`INSERT INTO edges (source_type, source_id, target_type, target_id, relation)
180
+ VALUES (@source_type, @source_id, @target_type, @target_id, 'inherits')`).run({
181
+ source_type: 'class',
182
+ source_id: classId,
183
+ target_type: 'class',
184
+ target_id: 0, // unresolved superclass
185
+ });
186
+ console.log(`🔗 Class ${cls.name} inherits ${cls.superClass} (edge stored for later resolution)`);
187
+ }
188
+ console.log(`🏷 Indexed class: ${cls.name} (id=${classId})`);
189
+ }
190
+ // Optional summary after extraction
191
+ console.log(`📊 Extraction summary for ${filePath}: ${functions.length} functions, ${classes.length} classes`);
112
192
  db.prepare(markFileAsExtractedTemplate).run({ id: fileId });
113
- log(`✅ Marked functions as extracted for ${filePath}`);
193
+ log(`✅ Marked functions/classes as extracted for ${filePath}`);
114
194
  return true;
115
195
  }
116
196
  catch (err) {
117
197
  log(`❌ Failed to extract from: ${filePath}`);
118
- log(` ↳ ${String(err.message)}`);
198
+ log(` ↳ ${err.message}`);
119
199
  db.prepare(markFileAsFailedTemplate).run({ id: fileId });
120
200
  return false;
121
201
  }