stackmem 1.0.0
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/README.md +223 -0
- package/dist/ast-index.js +272 -0
- package/dist/cli.js +718 -0
- package/dist/config.js +2 -0
- package/dist/device.js +30 -0
- package/dist/index.js +324 -0
- package/dist/scoring.js +217 -0
- package/dist/storage.js +53 -0
- package/dist/test-scoring.js +7 -0
- package/dist/tools.js +1 -0
- package/package.json +34 -0
- package/src/ast-index.ts +349 -0
- package/src/cli.ts +814 -0
- package/src/config.ts +3 -0
- package/src/device.ts +34 -0
- package/src/index.ts +436 -0
- package/src/scoring.ts +276 -0
- package/src/storage.ts +57 -0
- package/src/test-scoring.ts +11 -0
- package/src/tools.ts +0 -0
package/src/ast-index.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import Parser from "tree-sitter";
|
|
5
|
+
import TreeSitterTypeScript from "tree-sitter-typescript";
|
|
6
|
+
import TreeSitterJavaScript from "tree-sitter-javascript";
|
|
7
|
+
|
|
8
|
+
const TypeScript = TreeSitterTypeScript.typescript;
|
|
9
|
+
|
|
10
|
+
const SKIP_DIRS = new Set([
|
|
11
|
+
"node_modules",
|
|
12
|
+
".git",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
"out",
|
|
16
|
+
".next",
|
|
17
|
+
".turbo",
|
|
18
|
+
"coverage",
|
|
19
|
+
".coding-memory",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const FUNCTION_NODE_TYPES = new Set([
|
|
23
|
+
"function_declaration",
|
|
24
|
+
"function_expression",
|
|
25
|
+
"generator_function_declaration",
|
|
26
|
+
"generator_function",
|
|
27
|
+
"method_definition",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
interface FileExtraction {
|
|
31
|
+
relativePath: string;
|
|
32
|
+
functions: string[];
|
|
33
|
+
classes: string[];
|
|
34
|
+
imports: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getDbPath(projectPath: string): string {
|
|
38
|
+
return path.join(projectPath, ".coding-memory", "ast-index.db");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function openDb(projectPath: string): Database.Database {
|
|
42
|
+
const dbDir = path.join(projectPath, ".coding-memory");
|
|
43
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
44
|
+
const db = new Database(getDbPath(projectPath));
|
|
45
|
+
db.pragma("journal_mode = WAL");
|
|
46
|
+
db.exec(`
|
|
47
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
48
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
+
file_path TEXT NOT NULL UNIQUE,
|
|
50
|
+
file_name TEXT NOT NULL,
|
|
51
|
+
indexed_at TEXT NOT NULL
|
|
52
|
+
);
|
|
53
|
+
CREATE TABLE IF NOT EXISTS functions (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
56
|
+
name TEXT NOT NULL
|
|
57
|
+
);
|
|
58
|
+
CREATE TABLE IF NOT EXISTS classes (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
61
|
+
name TEXT NOT NULL
|
|
62
|
+
);
|
|
63
|
+
CREATE TABLE IF NOT EXISTS imports (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
66
|
+
source TEXT NOT NULL
|
|
67
|
+
);
|
|
68
|
+
`);
|
|
69
|
+
return db;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function walkProjectFiles(projectPath: string): string[] {
|
|
73
|
+
const results: string[] = [];
|
|
74
|
+
|
|
75
|
+
function walk(dir: string) {
|
|
76
|
+
let entries: fs.Dirent[];
|
|
77
|
+
try {
|
|
78
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
85
|
+
walk(path.join(dir, entry.name));
|
|
86
|
+
} else if (entry.isFile()) {
|
|
87
|
+
if (/\.(ts|js)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
88
|
+
results.push(path.join(dir, entry.name));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
walk(projectPath);
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function nameOfNode(node: Parser.SyntaxNode | null): string | null {
|
|
99
|
+
if (!node) return null;
|
|
100
|
+
return node.text;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractIdentifierFromLhs(node: Parser.SyntaxNode): string | null {
|
|
104
|
+
// For `const foo = () => {}` / `const foo = function () {}`, the
|
|
105
|
+
// enclosing variable_declarator's `name` field holds the identifier.
|
|
106
|
+
if (node.type === "identifier") return node.text;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractFromSource(sourceCode: string, language: unknown): {
|
|
111
|
+
functions: string[];
|
|
112
|
+
classes: string[];
|
|
113
|
+
imports: string[];
|
|
114
|
+
} {
|
|
115
|
+
const parser = new Parser();
|
|
116
|
+
parser.setLanguage(language);
|
|
117
|
+
const tree = parser.parse(sourceCode);
|
|
118
|
+
|
|
119
|
+
const functions: string[] = [];
|
|
120
|
+
const classes: string[] = [];
|
|
121
|
+
const imports: string[] = [];
|
|
122
|
+
|
|
123
|
+
function visit(node: Parser.SyntaxNode) {
|
|
124
|
+
switch (node.type) {
|
|
125
|
+
case "function_declaration":
|
|
126
|
+
case "generator_function_declaration": {
|
|
127
|
+
const name = nameOfNode(node.childForFieldName("name"));
|
|
128
|
+
if (name) functions.push(name);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "method_definition": {
|
|
132
|
+
const name = nameOfNode(node.childForFieldName("name"));
|
|
133
|
+
if (name) functions.push(name);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "function_expression":
|
|
137
|
+
case "generator_function":
|
|
138
|
+
case "arrow_function": {
|
|
139
|
+
const declaredName = nameOfNode(node.childForFieldName("name"));
|
|
140
|
+
if (declaredName) {
|
|
141
|
+
functions.push(declaredName);
|
|
142
|
+
} else if (node.parent?.type === "variable_declarator") {
|
|
143
|
+
const lhs = extractIdentifierFromLhs(node.parent.childForFieldName("name")!);
|
|
144
|
+
if (lhs) functions.push(lhs);
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "class_declaration":
|
|
149
|
+
case "class": {
|
|
150
|
+
const name = nameOfNode(node.childForFieldName("name"));
|
|
151
|
+
if (name) {
|
|
152
|
+
classes.push(name);
|
|
153
|
+
} else if (node.parent?.type === "variable_declarator") {
|
|
154
|
+
const lhs = extractIdentifierFromLhs(node.parent.childForFieldName("name")!);
|
|
155
|
+
if (lhs) classes.push(lhs);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "import_statement": {
|
|
160
|
+
const source = node.childForFieldName("source");
|
|
161
|
+
if (source) imports.push(source.text.replace(/^['"]|['"]$/g, ""));
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case "call_expression": {
|
|
165
|
+
const fn = node.childForFieldName("function");
|
|
166
|
+
if (fn?.type === "identifier" && fn.text === "require") {
|
|
167
|
+
const args = node.childForFieldName("arguments");
|
|
168
|
+
const firstArg = args?.namedChild(0);
|
|
169
|
+
if (firstArg?.type === "string") {
|
|
170
|
+
imports.push(firstArg.text.replace(/^['"]|['"]$/g, ""));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const child of node.namedChildren) {
|
|
178
|
+
visit(child);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
visit(tree.rootNode);
|
|
183
|
+
return { functions, classes, imports };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function extractFile(filePath: string, projectPath: string): FileExtraction {
|
|
187
|
+
const sourceCode = fs.readFileSync(filePath, "utf-8");
|
|
188
|
+
const language = filePath.endsWith(".ts") ? TypeScript : TreeSitterJavaScript;
|
|
189
|
+
const { functions, classes, imports } = extractFromSource(sourceCode, language);
|
|
190
|
+
return {
|
|
191
|
+
relativePath: path.relative(projectPath, filePath),
|
|
192
|
+
functions,
|
|
193
|
+
classes,
|
|
194
|
+
imports,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface AnalyzeCodebaseResult {
|
|
199
|
+
[key: string]: unknown;
|
|
200
|
+
files_indexed: number;
|
|
201
|
+
functions: number;
|
|
202
|
+
classes: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function analyzeCodebase(projectPath: string): AnalyzeCodebaseResult {
|
|
206
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
207
|
+
const db = openDb(resolvedProjectPath);
|
|
208
|
+
|
|
209
|
+
const insertFile = db.prepare(
|
|
210
|
+
"INSERT INTO files (file_path, file_name, indexed_at) VALUES (?, ?, ?) ON CONFLICT(file_path) DO UPDATE SET indexed_at = excluded.indexed_at RETURNING id"
|
|
211
|
+
);
|
|
212
|
+
const deleteChildren = db.prepare("DELETE FROM functions WHERE file_id = ?");
|
|
213
|
+
const deleteClasses = db.prepare("DELETE FROM classes WHERE file_id = ?");
|
|
214
|
+
const deleteImports = db.prepare("DELETE FROM imports WHERE file_id = ?");
|
|
215
|
+
const insertFunction = db.prepare("INSERT INTO functions (file_id, name) VALUES (?, ?)");
|
|
216
|
+
const insertClass = db.prepare("INSERT INTO classes (file_id, name) VALUES (?, ?)");
|
|
217
|
+
const insertImport = db.prepare("INSERT INTO imports (file_id, source) VALUES (?, ?)");
|
|
218
|
+
|
|
219
|
+
const files = walkProjectFiles(resolvedProjectPath);
|
|
220
|
+
let totalFunctions = 0;
|
|
221
|
+
let totalClasses = 0;
|
|
222
|
+
|
|
223
|
+
const runAll = db.transaction((filePaths: string[]) => {
|
|
224
|
+
for (const filePath of filePaths) {
|
|
225
|
+
const extraction = extractFile(filePath, resolvedProjectPath);
|
|
226
|
+
const row = insertFile.get(
|
|
227
|
+
extraction.relativePath,
|
|
228
|
+
path.basename(extraction.relativePath),
|
|
229
|
+
new Date().toISOString()
|
|
230
|
+
) as { id: number };
|
|
231
|
+
const fileId = row.id;
|
|
232
|
+
|
|
233
|
+
deleteChildren.run(fileId);
|
|
234
|
+
deleteClasses.run(fileId);
|
|
235
|
+
deleteImports.run(fileId);
|
|
236
|
+
|
|
237
|
+
for (const name of extraction.functions) {
|
|
238
|
+
insertFunction.run(fileId, name);
|
|
239
|
+
totalFunctions++;
|
|
240
|
+
}
|
|
241
|
+
for (const name of extraction.classes) {
|
|
242
|
+
insertClass.run(fileId, name);
|
|
243
|
+
totalClasses++;
|
|
244
|
+
}
|
|
245
|
+
for (const source of extraction.imports) {
|
|
246
|
+
insertImport.run(fileId, source);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
runAll(files);
|
|
252
|
+
db.close();
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
files_indexed: files.length,
|
|
256
|
+
functions: totalFunctions,
|
|
257
|
+
classes: totalClasses,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function findFileRow(
|
|
262
|
+
db: Database.Database,
|
|
263
|
+
fileName: string
|
|
264
|
+
): { id: number; file_path: string; file_name: string } | undefined {
|
|
265
|
+
const exact = db
|
|
266
|
+
.prepare("SELECT id, file_path, file_name FROM files WHERE file_path = ?")
|
|
267
|
+
.get(fileName) as { id: number; file_path: string; file_name: string } | undefined;
|
|
268
|
+
if (exact) return exact;
|
|
269
|
+
|
|
270
|
+
return db
|
|
271
|
+
.prepare("SELECT id, file_path, file_name FROM files WHERE file_name = ?")
|
|
272
|
+
.get(path.basename(fileName)) as
|
|
273
|
+
| { id: number; file_path: string; file_name: string }
|
|
274
|
+
| undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface FileSummary {
|
|
278
|
+
[key: string]: unknown;
|
|
279
|
+
file_path: string;
|
|
280
|
+
functions: string[];
|
|
281
|
+
classes: string[];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function getFileSummary(projectPath: string, fileName: string): FileSummary {
|
|
285
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
286
|
+
const dbPath = getDbPath(resolvedProjectPath);
|
|
287
|
+
if (!fs.existsSync(dbPath)) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`No AST index found at ${dbPath}. Run analyze_codebase on this project first.`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const db = new Database(dbPath, { readonly: true });
|
|
294
|
+
try {
|
|
295
|
+
const fileRow = findFileRow(db, fileName);
|
|
296
|
+
if (!fileRow) {
|
|
297
|
+
throw new Error(`File "${fileName}" not found in the AST index for ${resolvedProjectPath}.`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const functions = (
|
|
301
|
+
db.prepare("SELECT name FROM functions WHERE file_id = ?").all(fileRow.id) as {
|
|
302
|
+
name: string;
|
|
303
|
+
}[]
|
|
304
|
+
).map((r) => r.name);
|
|
305
|
+
const classes = (
|
|
306
|
+
db.prepare("SELECT name FROM classes WHERE file_id = ?").all(fileRow.id) as {
|
|
307
|
+
name: string;
|
|
308
|
+
}[]
|
|
309
|
+
).map((r) => r.name);
|
|
310
|
+
|
|
311
|
+
return { file_path: fileRow.file_path, functions, classes };
|
|
312
|
+
} finally {
|
|
313
|
+
db.close();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export interface FileDependencies {
|
|
318
|
+
[key: string]: unknown;
|
|
319
|
+
file_path: string;
|
|
320
|
+
imports: string[];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function findDependencies(projectPath: string, fileName: string): FileDependencies {
|
|
324
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
325
|
+
const dbPath = getDbPath(resolvedProjectPath);
|
|
326
|
+
if (!fs.existsSync(dbPath)) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`No AST index found at ${dbPath}. Run analyze_codebase on this project first.`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const db = new Database(dbPath, { readonly: true });
|
|
333
|
+
try {
|
|
334
|
+
const fileRow = findFileRow(db, fileName);
|
|
335
|
+
if (!fileRow) {
|
|
336
|
+
throw new Error(`File "${fileName}" not found in the AST index for ${resolvedProjectPath}.`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const imports = (
|
|
340
|
+
db.prepare("SELECT source FROM imports WHERE file_id = ?").all(fileRow.id) as {
|
|
341
|
+
source: string;
|
|
342
|
+
}[]
|
|
343
|
+
).map((r) => r.source);
|
|
344
|
+
|
|
345
|
+
return { file_path: fileRow.file_path, imports };
|
|
346
|
+
} finally {
|
|
347
|
+
db.close();
|
|
348
|
+
}
|
|
349
|
+
}
|