scai 0.1.116 → 0.1.117

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.
@@ -6,6 +6,8 @@ import { log } from '../../utils/log.js';
6
6
  import { markFileAsSkippedTemplate, markFileAsExtractedTemplate, markFileAsFailedTemplate, insertFunctionTemplate, insertGraphClassTemplate, insertEdgeTemplate, insertGraphEntityTagTemplate, insertGraphTagTemplate, selectGraphTagIdTemplate, } from '../sqlTemplates.js';
7
7
  import { getDbForRepo } from '../client.js';
8
8
  import { kgModule } from '../../pipeline/modules/kgModule.js';
9
+ import { BUILTINS } from '../../fileRules/builtins.js';
10
+ import { getUniqueId } from '../../utils/sharedUtils.js';
9
11
  function getFunctionName(node, parent, fileName) {
10
12
  if (node.id?.name)
11
13
  return node.id.name;
@@ -45,76 +47,75 @@ export async function extractFromJS(filePath, content, fileId) {
45
47
  FunctionDeclaration(node, ancestors) {
46
48
  const parent = ancestors[ancestors.length - 2];
47
49
  const name = getFunctionName(node, parent, path.basename(filePath));
48
- const uniqueId = name !== '<anon>'
49
- ? `${name}@${normalizedPath}`
50
- : `${path.basename(filePath)}:<anon>@${normalizedPath}:${node.loc?.start.line}`;
50
+ const funcContent = content.slice(node.start, node.end);
51
+ const unique_id = getUniqueId(name, filePath, node.loc?.start.line ?? -1, node.start, funcContent);
51
52
  functions.push({
52
53
  name,
53
54
  start_line: node.loc?.start.line ?? -1,
54
55
  end_line: node.loc?.end.line ?? -1,
55
- content: content.slice(node.start, node.end),
56
- uniqueId,
56
+ content: funcContent,
57
+ unique_id,
57
58
  });
58
59
  },
59
60
  FunctionExpression(node, ancestors) {
60
61
  const parent = ancestors[ancestors.length - 2];
61
62
  const name = getFunctionName(node, parent, path.basename(filePath));
62
- const uniqueId = name !== '<anon>'
63
- ? `${name}@${normalizedPath}`
64
- : `${path.basename(filePath)}:<anon>@${normalizedPath}:${node.loc?.start.line}`;
63
+ const funcContent = content.slice(node.start, node.end);
64
+ const unique_id = getUniqueId(name, filePath, node.loc?.start.line ?? -1, node.start, funcContent);
65
65
  functions.push({
66
66
  name,
67
67
  start_line: node.loc?.start.line ?? -1,
68
68
  end_line: node.loc?.end.line ?? -1,
69
- content: content.slice(node.start, node.end),
70
- uniqueId,
69
+ content: funcContent,
70
+ unique_id,
71
71
  });
72
72
  },
73
73
  ArrowFunctionExpression(node, ancestors) {
74
74
  const parent = ancestors[ancestors.length - 2];
75
75
  const name = getFunctionName(node, parent, path.basename(filePath));
76
- const uniqueId = name !== '<anon>'
77
- ? `${name}@${normalizedPath}`
78
- : `${path.basename(filePath)}:<anon>@${normalizedPath}:${node.loc?.start.line}`;
76
+ const funcContent = content.slice(node.start, node.end);
77
+ const unique_id = getUniqueId(name, filePath, node.loc?.start.line ?? -1, node.start, funcContent);
79
78
  functions.push({
80
79
  name,
81
80
  start_line: node.loc?.start.line ?? -1,
82
81
  end_line: node.loc?.end.line ?? -1,
83
- content: content.slice(node.start, node.end),
84
- uniqueId,
82
+ content: funcContent,
83
+ unique_id,
85
84
  });
86
85
  },
87
86
  ClassDeclaration(node) {
88
- const className = node.id?.name || '<anon-class>';
89
- const uniqueId = className !== '<anon-class>'
87
+ const className = node.id?.name || `${path.basename(filePath)}:<anon-class>`;
88
+ const classContent = content.slice(node.start, node.end);
89
+ const unique_id = node.id?.name
90
90
  ? `${className}@${normalizedPath}`
91
- : `${path.basename(filePath)}:<anon-class>@${normalizedPath}:${node.loc?.start.line}`;
91
+ : getUniqueId(className, filePath, node.loc?.start.line ?? -1, node.start, classContent);
92
92
  classes.push({
93
93
  name: className,
94
94
  start_line: node.loc?.start.line ?? -1,
95
95
  end_line: node.loc?.end.line ?? -1,
96
- content: content.slice(node.start, node.end),
96
+ content: classContent,
97
97
  superClass: node.superClass?.name ?? null,
98
- uniqueId,
98
+ unique_id,
99
99
  });
100
100
  },
101
101
  ClassExpression(node) {
102
- const className = node.id?.name || '<anon-class>';
103
- const uniqueId = className !== '<anon-class>'
102
+ const className = node.id?.name || `${path.basename(filePath)}:<anon-class>`;
103
+ const classContent = content.slice(node.start, node.end);
104
+ const unique_id = node.id?.name
104
105
  ? `${className}@${normalizedPath}`
105
- : `${path.basename(filePath)}:<anon-class>@${normalizedPath}:${node.loc?.start.line}`;
106
+ : getUniqueId(className, filePath, node.loc?.start.line ?? -1, node.start, classContent);
106
107
  classes.push({
107
108
  name: className,
108
109
  start_line: node.loc?.start.line ?? -1,
109
110
  end_line: node.loc?.end.line ?? -1,
110
- content: content.slice(node.start, node.end),
111
+ content: classContent,
111
112
  superClass: node.superClass?.name ?? null,
112
- uniqueId,
113
+ unique_id,
113
114
  });
114
115
  },
115
116
  });
116
117
  if (functions.length === 0 && classes.length === 0) {
117
- log(`⚠️ No functions/classes found in: ${filePath}`);
118
+ log(`⚠️ No functions/classes found in JS file: ${filePath}`);
118
119
  db.prepare(markFileAsSkippedTemplate).run({ id: fileId });
119
120
  return false;
120
121
  }
@@ -123,12 +124,12 @@ export async function extractFromJS(filePath, content, fileId) {
123
124
  try {
124
125
  const kgInput = { fileId, filepath: filePath, summary: undefined };
125
126
  const kgResult = await kgModule.run(kgInput, content);
126
- if (kgResult.entities?.length > 0) {
127
+ if (kgResult.entities?.length) {
127
128
  const insertTag = db.prepare(insertGraphTagTemplate);
128
129
  const getTagId = db.prepare(selectGraphTagIdTemplate);
129
130
  const insertEntityTag = db.prepare(insertGraphEntityTagTemplate);
130
131
  for (const entity of kgResult.entities) {
131
- if (!entity.type || !Array.isArray(entity.tags) || entity.tags.length === 0)
132
+ if (!entity.type || !Array.isArray(entity.tags) || !entity.tags.length)
132
133
  continue;
133
134
  for (const tag of entity.tags) {
134
135
  if (!tag || typeof tag !== 'string')
@@ -138,8 +139,8 @@ export async function extractFromJS(filePath, content, fileId) {
138
139
  const tagRow = getTagId.get({ name: tag });
139
140
  if (!tagRow)
140
141
  continue;
141
- const matchedUniqueId = functions.find(f => f.name === entity.name)?.uniqueId ||
142
- classes.find(c => c.name === entity.name)?.uniqueId ||
142
+ const matchedUniqueId = functions.find(f => f.name === entity.name)?.unique_id ||
143
+ classes.find(c => c.name === entity.name)?.unique_id ||
143
144
  `${entity.name}@${filePath}`;
144
145
  insertEntityTag.run({
145
146
  entity_type: entity.type,
@@ -158,7 +159,8 @@ export async function extractFromJS(filePath, content, fileId) {
158
159
  catch (kgErr) {
159
160
  console.warn(`⚠️ KG tagging failed for ${filePath}:`, kgErr instanceof Error ? kgErr.message : kgErr);
160
161
  }
161
- // --- Insert functions + edges ---
162
+ // --- Insert functions + call edges ---
163
+ const seenEdges = new Set();
162
164
  for (const fn of functions) {
163
165
  try {
164
166
  const embedding = await generateEmbedding(fn.content);
@@ -170,27 +172,44 @@ export async function extractFromJS(filePath, content, fileId) {
170
172
  content: fn.content,
171
173
  embedding: JSON.stringify(embedding),
172
174
  lang: 'js',
173
- unique_id: fn.uniqueId,
174
- });
175
- db.prepare(insertEdgeTemplate).run({
176
- source_type: 'file',
177
- source_unique_id: normalizedPath,
178
- target_type: 'function',
179
- target_unique_id: fn.uniqueId,
180
- relation: 'contains',
175
+ unique_id: fn.unique_id,
181
176
  });
177
+ // File -> Function 'contains' edge
178
+ const containsEdgeKey = `file->${fn.unique_id}`;
179
+ if (!seenEdges.has(containsEdgeKey)) {
180
+ db.prepare(insertEdgeTemplate).run({
181
+ source_type: 'file',
182
+ source_unique_id: normalizedPath,
183
+ target_type: 'function',
184
+ target_unique_id: fn.unique_id,
185
+ relation: 'contains',
186
+ });
187
+ seenEdges.add(containsEdgeKey);
188
+ }
189
+ // Call edges
182
190
  const fnAst = parse(fn.content, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
183
191
  walkAncestor(fnAst, {
184
192
  CallExpression(node) {
185
- const calleeName = node.callee?.name || 'unresolved';
193
+ const calleeName = node.callee?.name;
194
+ if (!calleeName || BUILTINS.has(calleeName))
195
+ return;
186
196
  const targetUniqueId = `${calleeName}@${normalizedPath}`;
187
- db.prepare(insertEdgeTemplate).run({
188
- source_type: 'function',
189
- source_unique_id: fn.uniqueId,
190
- target_type: 'function',
191
- target_unique_id: targetUniqueId,
192
- relation: 'calls',
193
- });
197
+ const edgeKey = `${fn.unique_id}->${targetUniqueId}`;
198
+ if (!seenEdges.has(edgeKey)) {
199
+ try {
200
+ db.prepare(insertEdgeTemplate).run({
201
+ source_type: 'function',
202
+ source_unique_id: fn.unique_id,
203
+ target_type: 'function',
204
+ target_unique_id: targetUniqueId,
205
+ relation: 'calls',
206
+ });
207
+ seenEdges.add(edgeKey);
208
+ }
209
+ catch (err) {
210
+ console.error('❌ Failed to persist call edge', { fn: fn.name, calleeName, error: err });
211
+ }
212
+ }
194
213
  },
195
214
  });
196
215
  log(`📌 Indexed JS function: ${fn.name}`);
@@ -211,19 +230,19 @@ export async function extractFromJS(filePath, content, fileId) {
211
230
  content: cls.content,
212
231
  embedding: JSON.stringify(embedding),
213
232
  lang: 'js',
214
- unique_id: cls.uniqueId,
233
+ unique_id: cls.unique_id,
215
234
  });
216
235
  db.prepare(insertEdgeTemplate).run({
217
236
  source_type: 'file',
218
237
  source_unique_id: normalizedPath,
219
238
  target_type: 'class',
220
- target_unique_id: cls.uniqueId,
239
+ target_unique_id: cls.unique_id,
221
240
  relation: 'contains',
222
241
  });
223
242
  if (cls.superClass) {
224
243
  db.prepare(insertEdgeTemplate).run({
225
244
  source_type: 'class',
226
- source_unique_id: cls.uniqueId,
245
+ source_unique_id: cls.unique_id,
227
246
  target_type: `unresolved:${cls.superClass}`,
228
247
  relation: 'inherits',
229
248
  });
@@ -237,22 +256,32 @@ export async function extractFromJS(filePath, content, fileId) {
237
256
  }
238
257
  // --- Imports / Exports edges ---
239
258
  for (const imp of imports) {
240
- db.prepare(insertEdgeTemplate).run({
241
- source_type: 'file',
242
- source_unique_id: normalizedPath,
243
- target_type: 'file',
244
- target_unique_id: `file@${imp}`,
245
- relation: 'imports',
246
- });
259
+ try {
260
+ db.prepare(insertEdgeTemplate).run({
261
+ source_type: 'file',
262
+ source_unique_id: normalizedPath,
263
+ target_type: 'file',
264
+ target_unique_id: `file@${imp}`,
265
+ relation: 'imports',
266
+ });
267
+ }
268
+ catch (err) {
269
+ console.error('❌ Failed to persist import edge', { imp, error: err });
270
+ }
247
271
  }
248
272
  for (const exp of exports) {
249
- db.prepare(insertEdgeTemplate).run({
250
- source_type: 'file',
251
- source_unique_id: normalizedPath,
252
- target_type: 'file',
253
- target_unique_id: `file@${exp}`,
254
- relation: 'exports',
255
- });
273
+ try {
274
+ db.prepare(insertEdgeTemplate).run({
275
+ source_type: 'file',
276
+ source_unique_id: normalizedPath,
277
+ target_type: 'file',
278
+ target_unique_id: `file@${exp}`,
279
+ relation: 'exports',
280
+ });
281
+ }
282
+ catch (err) {
283
+ console.error('❌ Failed to persist export edge', { exp, error: err });
284
+ }
256
285
  }
257
286
  log(`📊 Extraction summary for ${filePath}: ${functions.length} functions, ${classes.length} classes, ${imports.length} imports, ${exports.length} exports`);
258
287
  db.prepare(markFileAsExtractedTemplate).run({ id: fileId });
@@ -260,7 +289,7 @@ export async function extractFromJS(filePath, content, fileId) {
260
289
  return true;
261
290
  }
262
291
  catch (err) {
263
- log(`❌ Failed to extract from: ${filePath}`);
292
+ log(`❌ Failed to extract from JS file: ${filePath}`);
264
293
  log(` ↳ ${err.message}`);
265
294
  db.prepare(markFileAsFailedTemplate).run({ id: fileId });
266
295
  return false;
@@ -5,6 +5,8 @@ import { log } from '../../utils/log.js';
5
5
  import { getDbForRepo } from '../client.js';
6
6
  import { markFileAsSkippedTemplate, markFileAsExtractedTemplate, markFileAsFailedTemplate, insertFunctionTemplate, insertGraphClassTemplate, insertEdgeTemplate, insertGraphEntityTagTemplate, insertGraphTagTemplate, selectGraphTagIdTemplate, } from '../sqlTemplates.js';
7
7
  import { kgModule } from '../../pipeline/modules/kgModule.js';
8
+ import { BUILTINS } from '../../fileRules/builtins.js';
9
+ import { getUniqueId } from '../../utils/sharedUtils.js';
8
10
  export async function extractFromTS(filePath, content, fileId) {
9
11
  const db = getDbForRepo();
10
12
  const normalizedPath = path.normalize(filePath).replace(/\\/g, '/');
@@ -14,7 +16,7 @@ export async function extractFromTS(filePath, content, fileId) {
14
16
  // DTO arrays used to store into functions / graph_classes later (FTS index)
15
17
  const functions = [];
16
18
  const classes = [];
17
- // --- Gather all function AST nodes (so we can both index and analyze calls) ---
19
+ // --- Gather all function AST nodes ---
18
20
  const allFuncNodes = [
19
21
  ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration),
20
22
  ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionExpression),
@@ -25,26 +27,20 @@ export async function extractFromTS(filePath, content, fileId) {
25
27
  ...sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration),
26
28
  ...sourceFile.getDescendantsOfKind(SyntaxKind.ClassExpression),
27
29
  ];
28
- // If nothing found, mark as skipped
29
30
  if (allFuncNodes.length === 0 && allClassNodes.length === 0) {
30
31
  log(`⚠️ No functions/classes found in TS file: ${filePath}`);
31
32
  db.prepare(markFileAsSkippedTemplate).run({ id: fileId });
32
33
  return false;
33
34
  }
34
35
  log(`🔍 Found ${allFuncNodes.length} functions and ${allClassNodes.length} classes in ${filePath}`);
35
- // --- Optionally ask LLM (kgModule) for tags for the file, persist them and associate with entities if possible ---
36
+ // --- LLM tagging ---
36
37
  try {
37
- const kgInput = {
38
- fileId,
39
- filepath: filePath,
40
- summary: undefined,
41
- };
38
+ const kgInput = { fileId, filepath: filePath, summary: undefined };
42
39
  const kgResult = await kgModule.run(kgInput, content);
43
40
  if (kgResult.entities?.length > 0) {
44
41
  const insertTag = db.prepare(insertGraphTagTemplate);
45
42
  const getTagId = db.prepare(selectGraphTagIdTemplate);
46
43
  const insertEntityTag = db.prepare(insertGraphEntityTagTemplate);
47
- // We'll map entity names from the LLM to unique ids if we can (functions/classes); fallback to name@file
48
44
  for (const entity of kgResult.entities) {
49
45
  if (!entity.type || !Array.isArray(entity.tags) || entity.tags.length === 0)
50
46
  continue;
@@ -56,9 +52,8 @@ export async function extractFromTS(filePath, content, fileId) {
56
52
  const tagRow = getTagId.get({ name: tag });
57
53
  if (!tagRow)
58
54
  continue;
59
- // Try to match to an AST-extracted entity uniqueId, else fallback to name@filepath
60
- const matchedUniqueId = functions.find(f => f.name === entity.name)?.uniqueId ||
61
- classes.find(c => c.name === entity.name)?.uniqueId ||
55
+ const matchedUniqueId = functions.find(f => f.name === entity.name)?.unique_id ||
56
+ classes.find(c => c.name === entity.name)?.unique_id ||
62
57
  `${entity.name}@${filePath}`;
63
58
  insertEntityTag.run({
64
59
  entity_type: entity.type,
@@ -75,25 +70,17 @@ export async function extractFromTS(filePath, content, fileId) {
75
70
  }
76
71
  }
77
72
  catch (kgErr) {
78
- // tagging failure should not stop extraction; log and continue
79
73
  console.warn(`⚠️ KG tagging failed for ${filePath}:`, kgErr instanceof Error ? kgErr.message : kgErr);
80
74
  }
81
- // --- Process functions: build DTOs (for FTS) and emit call edges (KG) using AST nodes ---
82
- // inside the functions loop
75
+ // --- Process functions ---
83
76
  for (const funcNode of allFuncNodes) {
84
- // Resolve a reasonable function name
85
77
  const symbolName = funcNode.getSymbol()?.getName();
86
78
  const name = symbolName ?? '<anon>';
87
79
  const start = funcNode.getStartLineNumber();
88
80
  const end = funcNode.getEndLineNumber();
89
81
  const code = funcNode.getText();
90
- // --- updated uniqueId: include line number to make anonymous functions unique ---
91
- const uniqueId = symbolName
92
- ? `${symbolName}@${normalizedPath}`
93
- : `${path.basename(filePath)}:<anon>@${normalizedPath}:${start}`;
94
- // push DTO for later insertion into functions table
95
- functions.push({ name, start_line: start, end_line: end, content: code, uniqueId });
96
- // Persist function row
82
+ const unique_id = getUniqueId(name, filePath, start, funcNode.getStart(), code);
83
+ functions.push({ name, start_line: start, end_line: end, content: code, unique_id });
97
84
  try {
98
85
  const embedding = await generateEmbedding(code);
99
86
  db.prepare(insertFunctionTemplate).run({
@@ -104,7 +91,7 @@ export async function extractFromTS(filePath, content, fileId) {
104
91
  content: code,
105
92
  embedding: JSON.stringify(embedding),
106
93
  lang: 'ts',
107
- unique_id: uniqueId,
94
+ unique_id,
108
95
  });
109
96
  }
110
97
  catch (err) {
@@ -116,16 +103,17 @@ export async function extractFromTS(filePath, content, fileId) {
116
103
  source_type: 'file',
117
104
  source_unique_id: normalizedPath,
118
105
  target_type: 'function',
119
- target_unique_id: uniqueId,
106
+ target_unique_id: unique_id,
120
107
  relation: 'contains',
121
108
  });
122
109
  }
123
110
  catch (err) {
124
111
  console.error('❌ Failed to persist file->function edge', { filePath, name, error: err });
125
112
  }
126
- // --- AST-based detection of calls inside the function ---
113
+ // AST-based calls
127
114
  try {
128
115
  const callExprs = funcNode.getDescendantsOfKind(SyntaxKind.CallExpression);
116
+ const edgeSet = new Set();
129
117
  for (const callExpr of callExprs) {
130
118
  let calleeName;
131
119
  try {
@@ -135,36 +123,41 @@ export async function extractFromTS(filePath, content, fileId) {
135
123
  catch {
136
124
  calleeName = undefined;
137
125
  }
138
- const targetUniqueId = calleeName ? `${calleeName}@${normalizedPath}` : 'unresolved';
139
- try {
140
- db.prepare(insertEdgeTemplate).run({
141
- source_type: 'function',
142
- source_unique_id: uniqueId,
143
- target_type: 'function',
144
- target_unique_id: targetUniqueId,
145
- relation: 'calls',
146
- });
147
- }
148
- catch (err) {
149
- console.error('❌ Failed to persist call edge', { filePath, name, calleeName, error: err });
126
+ if (!calleeName || BUILTINS.has(calleeName))
127
+ continue;
128
+ const targetUniqueId = `${calleeName}@${normalizedPath}`;
129
+ const edgeKey = `${unique_id}->${targetUniqueId}`;
130
+ if (!edgeSet.has(edgeKey)) {
131
+ edgeSet.add(edgeKey);
132
+ try {
133
+ db.prepare(insertEdgeTemplate).run({
134
+ source_type: 'function',
135
+ source_unique_id: unique_id,
136
+ target_type: 'function',
137
+ target_unique_id: targetUniqueId,
138
+ relation: 'calls',
139
+ });
140
+ }
141
+ catch (err) {
142
+ console.error('❌ Failed to persist call edge', { filePath, name, calleeName, error: err });
143
+ }
150
144
  }
151
145
  }
152
146
  }
153
147
  catch (err) {
154
148
  console.warn(`⚠️ Failed to inspect call expressions for function ${name} in ${filePath}:`, err);
155
149
  }
156
- log(`📌 Indexed TS function: ${name} (uniqueId=${uniqueId})`);
150
+ log(`📌 Indexed TS function: ${name} (unique_id=${unique_id})`);
157
151
  }
158
- // --- Process classes (index + contains edge + inherits) ---
152
+ // --- Process classes ---
159
153
  for (const clsNode of allClassNodes) {
160
154
  const name = clsNode.getName() ?? `${path.basename(filePath)}:<anon-class>`;
161
155
  const start = clsNode.getStartLineNumber();
162
156
  const end = clsNode.getEndLineNumber();
163
157
  const code = clsNode.getText();
164
158
  const superClass = clsNode.getExtends()?.getText() ?? null;
165
- const uniqueId = `${name}@${normalizedPath}`;
166
- classes.push({ name, start_line: start, end_line: end, content: code, superClass, uniqueId });
167
- // persist class row
159
+ const unique_id = getUniqueId(name, filePath, start, clsNode.getStart(), code);
160
+ classes.push({ name, start_line: start, end_line: end, content: code, superClass, unique_id });
168
161
  try {
169
162
  const embedding = await generateEmbedding(code);
170
163
  db.prepare(insertGraphClassTemplate).run({
@@ -175,31 +168,29 @@ export async function extractFromTS(filePath, content, fileId) {
175
168
  content: code,
176
169
  embedding: JSON.stringify(embedding),
177
170
  lang: 'ts',
178
- unique_id: uniqueId,
171
+ unique_id,
179
172
  });
180
173
  }
181
174
  catch (err) {
182
175
  console.error('❌ Failed to insert graph class row', { filePath, name, error: err });
183
176
  }
184
- // File -> Class 'contains' edge
185
177
  try {
186
178
  db.prepare(insertEdgeTemplate).run({
187
179
  source_type: 'file',
188
180
  source_unique_id: normalizedPath,
189
181
  target_type: 'class',
190
- target_unique_id: uniqueId,
182
+ target_unique_id: unique_id,
191
183
  relation: 'contains',
192
184
  });
193
185
  }
194
186
  catch (err) {
195
187
  console.error('❌ Failed to persist file->class edge', { filePath, name, error: err });
196
188
  }
197
- // Inheritance edge (may be unresolved if superclass is in another file)
198
189
  if (superClass) {
199
190
  try {
200
191
  db.prepare(insertEdgeTemplate).run({
201
192
  source_type: 'class',
202
- source_unique_id: uniqueId,
193
+ source_unique_id: unique_id,
203
194
  target_type: 'class',
204
195
  target_unique_id: `unresolved:${superClass}`,
205
196
  relation: 'inherits',
@@ -210,16 +201,16 @@ export async function extractFromTS(filePath, content, fileId) {
210
201
  }
211
202
  log(`🔗 Class ${name} extends ${superClass} (edge stored for later resolution)`);
212
203
  }
213
- log(`🏷 Indexed TS class: ${name} (unique_id=${uniqueId})`);
214
- } // end classes loop
215
- // --- Handle imports (file-level) ---
204
+ log(`🏷 Indexed TS class: ${name} (unique_id=${unique_id})`);
205
+ }
206
+ // --- Imports ---
216
207
  try {
217
208
  const importDecls = sourceFile.getDescendantsOfKind(SyntaxKind.ImportDeclaration);
218
209
  for (const imp of importDecls) {
219
210
  const moduleSpecifier = imp.getModuleSpecifierValue();
220
211
  if (!moduleSpecifier)
221
212
  continue;
222
- const targetUniqueId = `file@${moduleSpecifier}`; // map to file unique id pattern
213
+ const targetUniqueId = `file@${moduleSpecifier}`;
223
214
  try {
224
215
  db.prepare(insertEdgeTemplate).run({
225
216
  source_type: 'file',
@@ -237,7 +228,7 @@ export async function extractFromTS(filePath, content, fileId) {
237
228
  catch (err) {
238
229
  console.warn(`⚠️ Import extraction failed for ${filePath}:`, err);
239
230
  }
240
- // --- Handle exports (file-level) ---
231
+ // --- Exports ---
241
232
  try {
242
233
  const exportDecls = sourceFile.getDescendantsOfKind(SyntaxKind.ExportDeclaration);
243
234
  for (const exp of exportDecls) {
@@ -262,7 +253,6 @@ export async function extractFromTS(filePath, content, fileId) {
262
253
  catch (err) {
263
254
  console.warn(`⚠️ Export extraction failed for ${filePath}:`, err);
264
255
  }
265
- // --- Mark file as extracted and finish ---
266
256
  db.prepare(markFileAsExtractedTemplate).run({ id: fileId });
267
257
  log(`📊 Extraction summary for ${filePath}: ${functions.length} functions, ${classes.length} classes`);
268
258
  log(`✅ Marked TS functions/classes as extracted for ${filePath}`);
@@ -44,8 +44,8 @@ export const searchFilesTemplate = `
44
44
  `;
45
45
  // --- Functions ---
46
46
  export const insertFunctionTemplate = `
47
- INSERT INTO functions (file_id, name, start_line, end_line, content, embedding, lang)
48
- VALUES (:file_id, :name, :start_line, :end_line, :content, :embedding, :lang)
47
+ INSERT INTO functions (file_id, name, start_line, end_line, content, embedding, lang, unique_id)
48
+ VALUES (:file_id, :name, :start_line, :end_line, :content, :embedding, :lang, :unique_id)
49
49
  `;
50
50
  // --- Graph ---
51
51
  export const insertGraphClassTemplate = `
@@ -0,0 +1,14 @@
1
+ export const BUILTINS = new Set([
2
+ // String methods
3
+ "trim", "toLowerCase", "toUpperCase", "includes", "startsWith", "endsWith", "replace", "match", "split",
4
+ // Array methods
5
+ "map", "forEach", "filter", "reduce", "some", "every", "find", "findIndex", "push", "pop", "shift", "unshift", "slice", "splice",
6
+ // Object methods
7
+ "keys", "values", "entries", "assign",
8
+ // Number/Math
9
+ "parseInt", "parseFloat", "isNaN", "isFinite", "floor", "ceil", "round", "abs", "max", "min", "random",
10
+ // JSON / Promise / RegExp
11
+ "stringify", "parse", "then", "catch", "finally", "test",
12
+ // Console
13
+ "log", "error", "warn", "info",
14
+ ]);
@@ -11,6 +11,7 @@ import { CONFIG_PATH } from './constants.js';
11
11
  const MODEL_PORT = 11434;
12
12
  const REQUIRED_MODELS = ['llama3:8b'];
13
13
  const OLLAMA_URL = 'https://ollama.com/download';
14
+ const VSCODE_URL = 'https://code.visualstudio.com/download';
14
15
  const isYesMode = process.argv.includes('--yes') || process.env.SCAI_YES === '1';
15
16
  let ollamaChecked = false;
16
17
  let ollamaAvailable = false;
@@ -38,7 +39,7 @@ function promptUser(question, timeout = 20000) {
38
39
  return new Promise((resolve) => {
39
40
  const timer = setTimeout(() => {
40
41
  rl.close();
41
- resolve(''); // treat empty as "continue"
42
+ resolve('');
42
43
  }, timeout);
43
44
  rl.question(question, (answer) => {
44
45
  clearTimeout(timer);
@@ -89,7 +90,7 @@ async function ensureOllamaRunning() {
89
90
  windowsHide: true,
90
91
  });
91
92
  child.unref();
92
- await new Promise((res) => setTimeout(res, 10000)); // give more time
93
+ await new Promise((res) => setTimeout(res, 10000));
93
94
  if (await isOllamaRunning()) {
94
95
  console.log(chalk.green('✅ Ollama started successfully.'));
95
96
  ollamaAvailable = true;
@@ -102,13 +103,11 @@ async function ensureOllamaRunning() {
102
103
  process.exit(1);
103
104
  }
104
105
  }
105
- // Ollama not detected; prompt user but allow continuing
106
106
  console.log(chalk.red('❌ Ollama is not installed or not in PATH.'));
107
107
  console.log(chalk.yellow(`📦 Ollama is required to run local AI models.`));
108
108
  const answer = await promptUser(`🌐 Recommended model: ${REQUIRED_MODELS.join(', ')}\nOpen download page in browser? (y/N): `);
109
- if (answer.toLowerCase() === 'y') {
109
+ if (answer.toLowerCase() === 'y')
110
110
  openBrowser(OLLAMA_URL);
111
- }
112
111
  await promptUser('⏳ Press Enter once Ollama is installed or to continue without it: ');
113
112
  if (await isOllamaRunning()) {
114
113
  console.log(chalk.green('✅ Ollama detected. Continuing...'));
@@ -116,7 +115,7 @@ async function ensureOllamaRunning() {
116
115
  }
117
116
  else {
118
117
  console.log(chalk.yellow('⚠️ Ollama not running. Models will not be available until installed.'));
119
- ollamaAvailable = false; // continue anyway
118
+ ollamaAvailable = false;
120
119
  }
121
120
  }
122
121
  // 🧰 List installed models
@@ -159,9 +158,49 @@ async function ensureModelsDownloaded() {
159
158
  }
160
159
  }
161
160
  }
161
+ // 🌟 Check if VSCode CLI is available
162
+ async function isVSCodeAvailable() {
163
+ try {
164
+ execSync('code --version', { stdio: 'ignore' });
165
+ return true;
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ // ⚡ Ensure VSCode CLI is installed
172
+ async function ensureVSCodeInstalled() {
173
+ if (await isVSCodeAvailable()) {
174
+ console.log(chalk.green('✅ VSCode CLI is available.'));
175
+ return;
176
+ }
177
+ console.log(chalk.red('❌ VSCode CLI not found.'));
178
+ const answer = await promptUser('Do you want to open the VSCode download page? (y/N): ');
179
+ if (answer.toLowerCase() === 'y')
180
+ openBrowser(VSCODE_URL);
181
+ await promptUser('VSCode CLI was not found. If you want to use VSCode features, please install it manually. ' +
182
+ 'Once installed, press Enter to continue. If you prefer to skip VSCode, just press Enter to continue without it: ');
183
+ if (await isVSCodeAvailable()) {
184
+ console.log(chalk.green('✅ VSCode CLI detected. Continuing...'));
185
+ }
186
+ else {
187
+ console.log(chalk.yellow('⚠️ VSCode CLI still not found. Some features may be disabled.'));
188
+ }
189
+ }
162
190
  // 🏁 Main bootstrap logic
163
191
  export async function bootstrap() {
164
192
  await autoInitIfNeeded();
165
193
  await ensureOllamaRunning();
166
194
  await ensureModelsDownloaded();
195
+ await ensureVSCodeInstalled();
196
+ }
197
+ // 🔗 Helper: open file or diff in VSCode
198
+ export function openInVSCode(filePath, options) {
199
+ const args = options?.diffWith ? ['--diff', options.diffWith, filePath] : [filePath];
200
+ try {
201
+ spawn('code', args, { stdio: 'inherit' });
202
+ }
203
+ catch {
204
+ console.log(chalk.red('❌ Failed to launch VSCode CLI. Make sure it is installed and in PATH.'));
205
+ }
167
206
  }
@@ -116,4 +116,154 @@ const sampleEntityTags = db.prepare(`
116
116
  sampleEntityTags.forEach(et => {
117
117
  console.log(` [${et.id}] ${et.entity_type}(${et.entity_id}) -> tag ${et.tag_id}`);
118
118
  });
119
+ // === Function → Edges Check ===
120
+ console.log("\n🕸 Function Calls / CalledBy consistency");
121
+ // 1. Check for edges pointing to missing functions
122
+ const missingFuncs = db.prepare(`
123
+ SELECT e.id, e.source_unique_id, e.target_unique_id
124
+ FROM graph_edges e
125
+ WHERE e.source_type = 'function'
126
+ AND NOT EXISTS (SELECT 1 FROM functions f WHERE f.unique_id = e.source_unique_id)
127
+ UNION
128
+ SELECT e.id, e.source_unique_id, e.target_unique_id
129
+ FROM graph_edges e
130
+ WHERE e.target_type = 'function'
131
+ AND NOT EXISTS (SELECT 1 FROM functions f WHERE f.unique_id = e.target_unique_id)
132
+ LIMIT 10
133
+ `).all();
134
+ if (missingFuncs.length === 0) {
135
+ console.log("✅ All edges reference valid functions.");
136
+ }
137
+ else {
138
+ console.log("❌ Found edges pointing to missing functions:");
139
+ missingFuncs.forEach(e => {
140
+ console.log(` Edge ${e.id}: ${e.source_unique_id} -> ${e.target_unique_id}`);
141
+ });
142
+ }
143
+ // 2. Functions with outgoing calls
144
+ const funcWithCalls = db.prepare(`
145
+ SELECT f.id, f.name, COUNT(e.id) AS callCount
146
+ FROM functions f
147
+ JOIN graph_edges e
148
+ ON e.source_type = 'function'
149
+ AND e.source_unique_id = f.unique_id
150
+ GROUP BY f.id
151
+ HAVING callCount > 0
152
+ ORDER BY callCount DESC
153
+ LIMIT 5
154
+ `).all();
155
+ funcWithCalls.forEach(f => {
156
+ console.log(` 🔹 Function [${f.id}] ${f.name} has ${f.callCount} outgoing calls`);
157
+ });
158
+ // 3. Functions with incoming calls
159
+ const funcWithCalledBy = db.prepare(`
160
+ SELECT f.id, f.name, COUNT(e.id) AS calledByCount
161
+ FROM functions f
162
+ JOIN graph_edges e
163
+ ON e.target_type = 'function'
164
+ AND e.target_unique_id = f.unique_id
165
+ GROUP BY f.id
166
+ HAVING calledByCount > 0
167
+ ORDER BY calledByCount DESC
168
+ LIMIT 5
169
+ `).all();
170
+ funcWithCalledBy.forEach(f => {
171
+ console.log(` 🔸 Function [${f.id}] ${f.name} is called by ${f.calledByCount} functions`);
172
+ });
173
+ // 4. Check for duplicate edges (same source→target)
174
+ const duplicateEdges = db.prepare(`
175
+ SELECT source_unique_id, target_unique_id, COUNT(*) as dupCount
176
+ FROM graph_edges
177
+ WHERE source_type = 'function' AND target_type = 'function'
178
+ GROUP BY source_unique_id, target_unique_id
179
+ HAVING dupCount > 1
180
+ LIMIT 5
181
+ `).all();
182
+ if (duplicateEdges.length > 0) {
183
+ console.log("⚠️ Duplicate function call edges found:");
184
+ duplicateEdges.forEach(d => {
185
+ console.log(` ${d.source_unique_id} -> ${d.target_unique_id} (x${d.dupCount})`);
186
+ });
187
+ }
188
+ else {
189
+ console.log("✅ No duplicate function call edges.");
190
+ }
191
+ // === File-specific check (AskCmd.ts) ===
192
+ const targetPath = "/Users/rzs/dev/repos/scai/cli/src/commands/AskCmd.ts";
193
+ // --- MUST KEEP: find the file row ---
194
+ const targetFile = db.prepare(`
195
+ SELECT id, path, filename, processing_status
196
+ FROM files
197
+ WHERE path = ?
198
+ `).get(targetPath);
199
+ if (!targetFile) {
200
+ console.log(`❌ File not found in DB: ${targetPath}`);
201
+ }
202
+ else {
203
+ console.log(`\n📂 File found: [${targetFile.id}] ${targetFile.filename} (${targetFile.processing_status})`);
204
+ // --- MUST KEEP: list functions in this file ---
205
+ const funcs = db.prepare(`
206
+ SELECT id, name, unique_id, start_line, end_line
207
+ FROM functions
208
+ WHERE file_id = ?
209
+ ORDER BY start_line
210
+ `).all(targetFile.id);
211
+ console.log(` Functions (${funcs.length}):`);
212
+ funcs.forEach((f) => {
213
+ console.log(` [${f.id}] ${f.name} (${f.unique_id}) lines ${f.start_line}-${f.end_line}`);
214
+ });
215
+ // --- OPTIONAL: outgoing calls from this file’s functions ---
216
+ const outgoing = db.prepare(`
217
+ SELECT f.name AS source_name, e.target_unique_id, e.relation
218
+ FROM functions f
219
+ JOIN graph_edges e
220
+ ON e.source_unique_id = f.unique_id
221
+ AND e.source_type = 'function'
222
+ WHERE f.file_id = ?
223
+ `).all(targetFile.id);
224
+ console.log(` Outgoing calls (${outgoing.length}):`);
225
+ outgoing.forEach((o) => {
226
+ console.log(` ${o.source_name} -[${o.relation}]-> ${o.target_unique_id}`);
227
+ });
228
+ // --- OPTIONAL: incoming calls to this file’s functions ---
229
+ const incoming = db.prepare(`
230
+ SELECT f.name AS target_name, e.source_unique_id, e.relation
231
+ FROM functions f
232
+ JOIN graph_edges e
233
+ ON e.target_unique_id = f.unique_id
234
+ AND e.target_type = 'function'
235
+ WHERE f.file_id = ?
236
+ `).all(targetFile.id);
237
+ console.log(` Incoming calls (${incoming.length}):`);
238
+ incoming.forEach((i) => {
239
+ console.log(` ${i.source_unique_id} -[${i.relation}]-> ${i.target_name}`);
240
+ });
241
+ // --- NICE TO HAVE: dangling edges check ---
242
+ const dangling = db.prepare(`
243
+ SELECT e.id, e.source_unique_id, e.target_unique_id, e.relation
244
+ FROM graph_edges e
245
+ WHERE e.source_unique_id IN (SELECT unique_id FROM functions WHERE file_id = ?)
246
+ OR e.target_unique_id IN (SELECT unique_id FROM functions WHERE file_id = ?)
247
+ `).all(targetFile.id, targetFile.id);
248
+ console.log(` Edges referencing this file's functions (dangling check): ${dangling.length}`);
249
+ dangling.forEach((d) => {
250
+ console.log(` Edge ${d.id}: ${d.source_unique_id} -[${d.relation}]-> ${d.target_unique_id}`);
251
+ });
252
+ // --- OPTIONAL: consistency check summary ---
253
+ if (funcs.length > 0 && outgoing.length === 0 && incoming.length === 0 && dangling.length === 0) {
254
+ console.log("⚠️ This file has functions but no graph edges. Possible causes:");
255
+ console.log(" - Edges were never extracted for this file, OR");
256
+ console.log(" - unique_id mismatch between functions and edges.");
257
+ }
258
+ else {
259
+ console.log("✅ Edges and functions are consistent for this file.");
260
+ }
261
+ const funcsDebug = db.prepare(`
262
+ SELECT id, name, unique_id
263
+ FROM functions
264
+ WHERE file_id = ?
265
+ `).all(targetFile.id);
266
+ console.log("Funcsdebug:\n");
267
+ console.log(funcsDebug);
268
+ }
119
269
  console.log("\n✅ DB check completed.\n");
@@ -106,20 +106,24 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
106
106
  const kgTree = buildFileTree({ id: topFile.id, path: topFile.path, summary: topFile.summary }, kgDepth);
107
107
  promptSections.push(`**KG-Related Files (JSON tree, depth ${kgDepth}):**\n\`\`\`json\n${JSON.stringify(kgTree, null, 2)}\n\`\`\``);
108
108
  const functionCallsAll = db.prepare(`
109
- SELECT source_unique_id, target_unique_id
110
- FROM graph_edges
111
- WHERE source_type = 'function' AND relation = 'calls'
112
- AND source_unique_id IN (
113
- SELECT unique_id FROM functions WHERE file_id = ?
114
- )
115
- `).all(topFile.id);
109
+ SELECT source_unique_id, target_unique_id
110
+ FROM graph_edges
111
+ WHERE source_type = 'function' AND relation = 'calls'
112
+ AND source_unique_id IN (
113
+ SELECT unique_id FROM functions WHERE file_id = ?
114
+ )
115
+ `).all(topFile.id);
116
116
  const callsByFunction = {};
117
117
  for (const fn of functionRows) {
118
118
  const rows = functionCallsAll
119
119
  .filter(r => r.source_unique_id === fn.unique_id)
120
120
  .slice(0, FUNCTION_LIMIT);
121
+ // Truncate function content for preview
122
+ const lines = fn.content?.split("\n").map(l => l.trim()).filter(Boolean) || ["[no content]"];
123
+ const preview = lines.slice(0, 3).map(l => l.slice(0, 200) + (l.length > 200 ? "…" : "")).join(" | ");
121
124
  callsByFunction[fn.name || fn.unique_id] = {
122
125
  calls: rows.map(r => ({ unique_id: r.target_unique_id })),
126
+ preview,
123
127
  };
124
128
  }
125
129
  if (Object.keys(callsByFunction).length > 0) {
@@ -127,20 +131,24 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
127
131
  }
128
132
  // --- Function-level "called by" overview (limited) ---
129
133
  const calledByAll = db.prepare(`
130
- SELECT source_unique_id, target_unique_id
131
- FROM graph_edges
132
- WHERE target_type = 'function' AND relation = 'calls'
133
- AND target_unique_id IN (
134
- SELECT unique_id FROM functions WHERE file_id = ?
135
- )
136
- `).all(topFile.id);
134
+ SELECT source_unique_id, target_unique_id
135
+ FROM graph_edges
136
+ WHERE target_type = 'function' AND relation = 'calls'
137
+ AND target_unique_id IN (
138
+ SELECT unique_id FROM functions WHERE file_id = ?
139
+ )
140
+ `).all(topFile.id);
137
141
  const calledByByFunction = {};
138
142
  for (const fn of functionRows) {
139
143
  const rows = calledByAll
140
144
  .filter(r => r.target_unique_id === fn.unique_id)
141
145
  .slice(0, FUNCTION_LIMIT);
146
+ // Reuse truncated preview
147
+ const lines = fn.content?.split("\n").map(l => l.trim()).filter(Boolean) || ["[no content]"];
148
+ const preview = lines.slice(0, 3).map(l => l.slice(0, 200) + (l.length > 200 ? "…" : "")).join(" | ");
142
149
  calledByByFunction[fn.name || fn.unique_id] = {
143
150
  calledBy: rows.map(r => ({ unique_id: r.source_unique_id })),
151
+ preview,
144
152
  };
145
153
  }
146
154
  if (Object.keys(calledByByFunction).length > 0) {
@@ -0,0 +1,8 @@
1
+ import path from 'path';
2
+ import crypto from 'crypto';
3
+ // put this helper at top-level (or import from shared utils)
4
+ export function getUniqueId(name, filePath, startLine, startColumn, content) {
5
+ const normalizedPath = path.normalize(filePath).replace(/\\/g, '/');
6
+ const hash = crypto.createHash('md5').update(content).digest('hex').slice(0, 6);
7
+ return `${name}@${normalizedPath}:${startLine}:${startColumn}:${hash}`;
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.116",
3
+ "version": "0.1.117",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"