scai 0.1.115 → 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,75 +6,89 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
6
6
  const log = (...args) => console.log("[buildContextualPrompt]", ...args);
7
7
  const promptSections = [];
8
8
  const seenPaths = new Set();
9
- // --- Utility: Summarize text to a few words ---
10
9
  function summarizeForPrompt(summary, maxWords = 30) {
11
10
  if (!summary)
12
11
  return undefined;
13
12
  const words = summary.split(/\s+/);
14
13
  return words.length <= maxWords ? summary.trim() : words.slice(0, maxWords).join(" ") + " …";
15
14
  }
16
- // --- Step 1: Top file summary ---
15
+ // --- Top file summary ---
17
16
  if (topFile.summary) {
18
17
  promptSections.push(`**Top file:** ${topFile.path}\n${topFile.summary}`);
19
18
  seenPaths.add(topFile.path);
20
19
  }
21
- // ===========================
22
- // SECTION A: Database queries
23
- // ===========================
24
- // --- Step 2a: KG entities/tags ---
20
+ // --- Knowledge Graph tags ---
25
21
  const topEntitiesRows = db.prepare(`
26
- SELECT et.entity_type, et.entity_id, tm.name AS tag
27
- FROM entity_tags et
28
- JOIN tags_master tm ON et.tag_id = tm.id
29
- WHERE et.entity_id = ?
30
- `).all(topFile.id);
22
+ SELECT et.entity_type, et.entity_unique_id, tm.name AS tag
23
+ FROM graph_entity_tags et
24
+ JOIN graph_tags_master tm ON et.tag_id = tm.id
25
+ WHERE et.entity_unique_id = ?
26
+ `).all(topFile.path);
31
27
  if (topEntitiesRows.length > 0) {
32
28
  const tags = topEntitiesRows.map(r => `- **${r.entity_type}**: ${r.tag}`);
33
29
  promptSections.push(`**Knowledge Graph context for ${topFile.path}:**\n${tags.join("\n")}`);
34
30
  }
35
- // --- Step 2b: Functions in this file ---
31
+ // --- Imports / Exports ---
32
+ const imports = db.prepare(`
33
+ SELECT e.target_unique_id AS imported
34
+ FROM graph_edges e
35
+ WHERE e.source_type = 'file'
36
+ AND e.relation = 'imports'
37
+ AND e.source_unique_id = ?
38
+ `).all(topFile.path);
39
+ if (imports.length) {
40
+ promptSections.push(`**Imports from ${topFile.path}:**\n` + imports.map(i => `- ${i.imported}`).join("\n"));
41
+ }
42
+ const exports = db.prepare(`
43
+ SELECT e.target_unique_id AS exported
44
+ FROM graph_edges e
45
+ WHERE e.source_type = 'file'
46
+ AND e.relation = 'exports'
47
+ AND e.source_unique_id = ?
48
+ `).all(topFile.path);
49
+ if (exports.length) {
50
+ promptSections.push(`**Exports from ${topFile.path}:**\n` + exports.map(e => `- ${e.exported}`).join("\n"));
51
+ }
52
+ // --- Functions in file ---
36
53
  const functionRows = db
37
- .prepare(`SELECT name, start_line, end_line, content FROM functions WHERE file_id = ? ORDER BY start_line`)
54
+ .prepare(`SELECT unique_id, name, start_line, end_line, content FROM functions WHERE file_id = ? ORDER BY start_line`)
38
55
  .all(topFile.id);
39
- const FUNCTION_LIMIT = 15;
56
+ const FUNCTION_LIMIT = 5;
40
57
  const hasMoreFunctions = functionRows.length > FUNCTION_LIMIT;
41
58
  const functionsSummary = functionRows.slice(0, FUNCTION_LIMIT).map(f => {
42
59
  const lines = f.content?.split("\n").map(l => l.trim()).filter(Boolean) || ["[no content]"];
43
- const preview = lines.slice(0, 3) // first 3 lines
44
- .map(l => l.slice(0, 200) + (l.length > 200 ? "…" : ""))
45
- .join(" | ");
60
+ const preview = lines.slice(0, 3).map(l => l.slice(0, 200) + (l.length > 200 ? "…" : "")).join(" | ");
46
61
  return `- ${f.name || "[anonymous]"} (lines ${f.start_line}-${f.end_line}) — ${preview}`;
47
62
  });
48
63
  if (functionsSummary.length) {
49
64
  promptSections.push(`**Functions in ${topFile.path} (showing ${functionsSummary.length}${hasMoreFunctions ? ` of ${functionRows.length}` : ""}):**\n${functionsSummary.join("\n")}`);
50
65
  }
51
66
  // ===============================
52
- // SECTION B: Graph / KG traversal
67
+ // Graph / KG traversal
53
68
  // ===============================
54
- const kgRelatedStmt = db.prepare(`
55
- SELECT DISTINCT f.id, f.path, f.summary
56
- FROM edges e
57
- JOIN files f ON e.target_id = f.id
58
- WHERE e.source_type = 'file'
59
- AND e.target_type = 'file'
60
- AND e.source_id = ?
61
- `);
62
- function getRelatedKGFiles(fileId, visited = new Set()) {
63
- if (visited.has(fileId))
69
+ function getRelatedKGFiles(fileUniqueId, visited = new Set()) {
70
+ if (visited.has(fileUniqueId))
64
71
  return [];
65
- visited.add(fileId);
66
- const rows = kgRelatedStmt.all(fileId);
72
+ visited.add(fileUniqueId);
73
+ const rows = db.prepare(`
74
+ SELECT DISTINCT f.id, f.path, f.summary
75
+ FROM graph_edges e
76
+ JOIN files f ON e.target_unique_id = f.path
77
+ WHERE e.source_type = 'file'
78
+ AND e.target_type = 'file'
79
+ AND e.source_unique_id = ?
80
+ `).all(fileUniqueId);
67
81
  let results = [];
68
82
  for (const row of rows) {
69
83
  results.push(row);
70
- results.push(...getRelatedKGFiles(row.id, visited));
84
+ results.push(...getRelatedKGFiles(row.path, visited));
71
85
  }
72
86
  return results;
73
87
  }
74
88
  function buildFileTree(file, depth, visited = new Set()) {
75
- if (visited.has(file.id))
89
+ if (visited.has(file.path))
76
90
  return { id: file.id.toString(), path: file.path };
77
- visited.add(file.id);
91
+ visited.add(file.path);
78
92
  const maxWordsByDepth = depth >= 3 ? 30 : depth === 2 ? 15 : 0;
79
93
  const node = {
80
94
  id: file.id.toString(),
@@ -82,9 +96,7 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
82
96
  summary: maxWordsByDepth > 0 ? summarizeForPrompt(file.summary, maxWordsByDepth) : undefined,
83
97
  };
84
98
  if (depth > 1) {
85
- const relatedFiles = getRelatedKGFiles(file.id, visited)
86
- .map(f => ({ id: f.id, path: f.path, summary: f.summary }))
87
- .slice(0, 5); // cap children
99
+ const relatedFiles = getRelatedKGFiles(file.path).map(f => ({ id: f.id, path: f.path, summary: f.summary })).slice(0, 5);
88
100
  const relatedNodes = relatedFiles.map(f => buildFileTree(f, depth - 1, visited));
89
101
  if (relatedNodes.length)
90
102
  node.related = relatedNodes;
@@ -93,7 +105,56 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
93
105
  }
94
106
  const kgTree = buildFileTree({ id: topFile.id, path: topFile.path, summary: topFile.summary }, kgDepth);
95
107
  promptSections.push(`**KG-Related Files (JSON tree, depth ${kgDepth}):**\n\`\`\`json\n${JSON.stringify(kgTree, null, 2)}\n\`\`\``);
96
- // --- Step 3: File tree (shallow) ---
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);
116
+ const callsByFunction = {};
117
+ for (const fn of functionRows) {
118
+ const rows = functionCallsAll
119
+ .filter(r => r.source_unique_id === fn.unique_id)
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(" | ");
124
+ callsByFunction[fn.name || fn.unique_id] = {
125
+ calls: rows.map(r => ({ unique_id: r.target_unique_id })),
126
+ preview,
127
+ };
128
+ }
129
+ if (Object.keys(callsByFunction).length > 0) {
130
+ promptSections.push(`**Function-level calls (limited, JSON):**\n\`\`\`json\n${JSON.stringify(callsByFunction, null, 2)}\n\`\`\``);
131
+ }
132
+ // --- Function-level "called by" overview (limited) ---
133
+ const calledByAll = db.prepare(`
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);
141
+ const calledByByFunction = {};
142
+ for (const fn of functionRows) {
143
+ const rows = calledByAll
144
+ .filter(r => r.target_unique_id === fn.unique_id)
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(" | ");
149
+ calledByByFunction[fn.name || fn.unique_id] = {
150
+ calledBy: rows.map(r => ({ unique_id: r.source_unique_id })),
151
+ preview,
152
+ };
153
+ }
154
+ if (Object.keys(calledByByFunction).length > 0) {
155
+ promptSections.push(`**Function-level called-by (limited, JSON):**\n\`\`\`json\n${JSON.stringify(calledByByFunction, null, 2)}\n\`\`\``);
156
+ }
157
+ // --- Focused file tree (shallow) ---
97
158
  try {
98
159
  const fileTree = generateFocusedFileTree(topFile.path, 2);
99
160
  if (fileTree) {
@@ -103,7 +164,7 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
103
164
  catch (e) {
104
165
  console.warn("⚠️ Could not generate file tree:", e);
105
166
  }
106
- // --- Step 4: Optional code snippet ---
167
+ // --- Optional code snippet ---
107
168
  const MAX_LINES = 50;
108
169
  const queryNeedsCode = /\b(code|implementation|function|snippet)\b/i.test(query);
109
170
  if ((!topFile.summary || queryNeedsCode) && topFile.code) {
@@ -113,7 +174,7 @@ export async function buildContextualPrompt({ topFile, query, kgDepth = 3, }) {
113
174
  snippet += "\n... [truncated]";
114
175
  promptSections.push(`**Code Context (first ${MAX_LINES} lines):**\n\`\`\`\n${snippet}\n\`\`\``);
115
176
  }
116
- // --- Step 5: User query ---
177
+ // --- User query ---
117
178
  promptSections.push(`**Query:** ${query}`);
118
179
  const promptText = promptSections.join("\n\n---\n\n");
119
180
  log("✅ Contextual prompt built for:", topFile.path);
@@ -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.115",
3
+ "version": "0.1.117",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -1,15 +0,0 @@
1
- // src/commands/MigrateCmd.ts
2
- import path from 'path';
3
- import { pathToFileURL } from 'url';
4
- import { fileURLToPath } from 'url';
5
- export async function runMigrateCommand() {
6
- const scriptPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../dist/scripts/migrateDb.js' // compiled output
7
- );
8
- try {
9
- await import(pathToFileURL(scriptPath).href);
10
- }
11
- catch (err) {
12
- console.error('❌ Failed to run migration script:', err);
13
- process.exit(1);
14
- }
15
- }