@dailephd/my-dev-kit 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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/dist/cli.js +2611 -0
- package/docs/ARCHITECTURE.md +649 -0
- package/docs/CI_CD.md +248 -0
- package/docs/COMMANDS.md +684 -0
- package/docs/DEVELOPMENT.md +360 -0
- package/docs/GRAPH_SCHEMA.md +675 -0
- package/docs/QUICKSTART.md +243 -0
- package/docs/RELEASE.md +249 -0
- package/docs/ROADMAP.md +733 -0
- package/docs/SECURITY.md +92 -0
- package/docs/WORKFLOWS.md +316 -0
- package/examples/README.md +23 -0
- package/examples/basic-python/README.md +14 -0
- package/examples/basic-python/src/main.py +38 -0
- package/examples/basic-python/src/utils.py +24 -0
- package/examples/basic-ts/README.md +14 -0
- package/examples/basic-ts/src/index.ts +6 -0
- package/examples/basic-ts/src/userService.ts +9 -0
- package/examples/basic-ts/src/userTypes.ts +6 -0
- package/package.json +51 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2611 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/indexing/runIndexCommand.ts
|
|
7
|
+
import * as fs4 from "fs";
|
|
8
|
+
import * as path9 from "path";
|
|
9
|
+
|
|
10
|
+
// src/graph/codeGraphTypes.ts
|
|
11
|
+
var CODE_GRAPH_SCHEMA_VERSION = "1.0.0";
|
|
12
|
+
|
|
13
|
+
// src/graph/buildCodeGraph.ts
|
|
14
|
+
function buildCodeGraph(options) {
|
|
15
|
+
const nodes = [];
|
|
16
|
+
const edges = [];
|
|
17
|
+
const symbolIds = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const file of options.symbolIndex.files) {
|
|
19
|
+
const fileId = fileNodeId(file.path);
|
|
20
|
+
nodes.push({
|
|
21
|
+
id: fileId,
|
|
22
|
+
kind: "file",
|
|
23
|
+
label: basename(file.path),
|
|
24
|
+
path: file.path,
|
|
25
|
+
language: file.language
|
|
26
|
+
});
|
|
27
|
+
for (const symbol of file.symbols) {
|
|
28
|
+
const symbolId = symbolNodeId(file.path, symbol.name);
|
|
29
|
+
symbolIds.set(symbolKey(file.path, symbol), symbolId);
|
|
30
|
+
nodes.push({
|
|
31
|
+
id: symbolId,
|
|
32
|
+
kind: "symbol",
|
|
33
|
+
label: symbol.name,
|
|
34
|
+
path: file.path,
|
|
35
|
+
symbolName: symbol.name,
|
|
36
|
+
symbolKind: symbol.kind,
|
|
37
|
+
line: symbol.location.line,
|
|
38
|
+
exported: symbol.exported
|
|
39
|
+
});
|
|
40
|
+
edges.push({
|
|
41
|
+
id: edgeId(fileId, "defines", symbolId),
|
|
42
|
+
source: fileId,
|
|
43
|
+
target: symbolId,
|
|
44
|
+
kind: "defines"
|
|
45
|
+
});
|
|
46
|
+
if (symbol.exported) {
|
|
47
|
+
edges.push({
|
|
48
|
+
id: edgeId(fileId, "exports", symbolId),
|
|
49
|
+
source: fileId,
|
|
50
|
+
target: symbolId,
|
|
51
|
+
kind: "exports"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const dep of options.symbolIndex.graph?.fileDeps ?? []) {
|
|
57
|
+
const source = fileNodeId(dep.from);
|
|
58
|
+
const target = fileNodeId(dep.to);
|
|
59
|
+
edges.push({
|
|
60
|
+
id: edgeId(source, "imports", target),
|
|
61
|
+
source,
|
|
62
|
+
target,
|
|
63
|
+
kind: dep.kind === "import" ? "imports" : "depends-on",
|
|
64
|
+
label: dep.kind
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const call of options.callGraph?.edges ?? []) {
|
|
68
|
+
if (!call.callee.file) continue;
|
|
69
|
+
const source = symbolIds.get(`${call.caller.file}#${call.caller.name}@${call.caller.line}`);
|
|
70
|
+
const target = findSymbolId(symbolIds, call.callee.file, call.callee.name);
|
|
71
|
+
if (!source || !target) continue;
|
|
72
|
+
edges.push({
|
|
73
|
+
id: `${edgeId(source, "calls", target)}@${call.callLine}`,
|
|
74
|
+
source,
|
|
75
|
+
target,
|
|
76
|
+
kind: "calls"
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
sortById(nodes);
|
|
80
|
+
sortById(edges);
|
|
81
|
+
const fileNodeCount = nodes.filter((node) => node.kind === "file").length;
|
|
82
|
+
const symbolNodeCount = nodes.filter((node) => node.kind === "symbol").length;
|
|
83
|
+
return {
|
|
84
|
+
artifactKind: "code-graph",
|
|
85
|
+
schemaVersion: CODE_GRAPH_SCHEMA_VERSION,
|
|
86
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
87
|
+
nodes,
|
|
88
|
+
edges,
|
|
89
|
+
summary: {
|
|
90
|
+
nodeCount: nodes.length,
|
|
91
|
+
edgeCount: edges.length,
|
|
92
|
+
fileNodeCount,
|
|
93
|
+
symbolNodeCount
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function fileNodeId(filePath) {
|
|
98
|
+
return `file:${filePath}`;
|
|
99
|
+
}
|
|
100
|
+
function symbolNodeId(filePath, name) {
|
|
101
|
+
return `symbol:${filePath}#${name}`;
|
|
102
|
+
}
|
|
103
|
+
function symbolKey(filePath, symbol) {
|
|
104
|
+
return `${filePath}#${symbol.name}@${symbol.location.line}`;
|
|
105
|
+
}
|
|
106
|
+
function findSymbolId(symbolIds, filePath, name) {
|
|
107
|
+
const prefix = `${filePath}#${name}@`;
|
|
108
|
+
for (const [key, id] of symbolIds) {
|
|
109
|
+
if (key.startsWith(prefix)) return id;
|
|
110
|
+
}
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
function edgeId(source, kind, target) {
|
|
114
|
+
return `${source}--${kind}-->${target}`;
|
|
115
|
+
}
|
|
116
|
+
function basename(filePath) {
|
|
117
|
+
return filePath.split("/").at(-1) ?? filePath;
|
|
118
|
+
}
|
|
119
|
+
function sortById(items) {
|
|
120
|
+
items.sort((a, b) => a.id.localeCompare(b.id));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/io/pathUtils.ts
|
|
124
|
+
import * as path from "path";
|
|
125
|
+
function toForwardSlash(p) {
|
|
126
|
+
return p.replace(/\\/g, "/");
|
|
127
|
+
}
|
|
128
|
+
function isInsideRoot(root, target) {
|
|
129
|
+
const absRoot = path.resolve(root);
|
|
130
|
+
const absTarget = path.resolve(target);
|
|
131
|
+
const rel = path.relative(absRoot, absTarget);
|
|
132
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/symbol-index/builder.ts
|
|
136
|
+
import * as fs2 from "fs";
|
|
137
|
+
|
|
138
|
+
// src/symbol-index/graphBuilder.ts
|
|
139
|
+
import * as ts from "typescript";
|
|
140
|
+
import * as path2 from "path";
|
|
141
|
+
|
|
142
|
+
// src/symbol-index/types.ts
|
|
143
|
+
var SCHEMA_VERSION = "2";
|
|
144
|
+
|
|
145
|
+
// src/symbol-index/graphBuilder.ts
|
|
146
|
+
function createCallGraph(edges, buildTime = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
147
|
+
return {
|
|
148
|
+
schemaVersion: SCHEMA_VERSION,
|
|
149
|
+
buildTime,
|
|
150
|
+
edgeCount: edges.length,
|
|
151
|
+
edges
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function extractTypeScriptCallGraphEdges(files) {
|
|
155
|
+
const edges = [];
|
|
156
|
+
const exportMap = buildExportMap(files);
|
|
157
|
+
for (const { filePath, sourceText } of files) {
|
|
158
|
+
const sourceFile = ts.createSourceFile(
|
|
159
|
+
filePath,
|
|
160
|
+
sourceText,
|
|
161
|
+
ts.ScriptTarget.Latest,
|
|
162
|
+
true
|
|
163
|
+
);
|
|
164
|
+
const importedNames = buildImportedNameMap(sourceFile, filePath, exportMap);
|
|
165
|
+
extractEdges(sourceFile, filePath, importedNames, edges);
|
|
166
|
+
}
|
|
167
|
+
return edges;
|
|
168
|
+
}
|
|
169
|
+
function extractEdges(sourceFile, filePath, importedNames, edges) {
|
|
170
|
+
function walk(node, currentCaller) {
|
|
171
|
+
const newCaller = resolveCaller(node, sourceFile, filePath, currentCaller);
|
|
172
|
+
const activeCaller = newCaller ?? currentCaller;
|
|
173
|
+
if (ts.isCallExpression(node) && activeCaller) {
|
|
174
|
+
const calleeName = calleeNameOf(node.expression);
|
|
175
|
+
if (calleeName && calleeName !== "<computed>") {
|
|
176
|
+
const callLine = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
177
|
+
const calleeFile = resolveCalleeFile(calleeName, importedNames);
|
|
178
|
+
edges.push({
|
|
179
|
+
caller: {
|
|
180
|
+
file: activeCaller.file,
|
|
181
|
+
name: activeCaller.name,
|
|
182
|
+
line: activeCaller.line
|
|
183
|
+
},
|
|
184
|
+
callee: { file: calleeFile, name: calleeName },
|
|
185
|
+
callLine
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
ts.forEachChild(node, (child) => walk(child, activeCaller));
|
|
190
|
+
}
|
|
191
|
+
walk(sourceFile, null);
|
|
192
|
+
}
|
|
193
|
+
function resolveCaller(node, sourceFile, filePath, parent) {
|
|
194
|
+
const lineOf = (n) => sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line + 1;
|
|
195
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
196
|
+
return { file: filePath, name: node.name.text, line: lineOf(node) };
|
|
197
|
+
}
|
|
198
|
+
if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
199
|
+
const ownerName = parent?.name ? `${parent.name}.${node.name.text}` : node.name.text;
|
|
200
|
+
return { file: filePath, name: ownerName, line: lineOf(node) };
|
|
201
|
+
}
|
|
202
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
203
|
+
if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
|
|
204
|
+
return { file: filePath, name: node.parent.name.text, line: lineOf(node) };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
function calleeNameOf(expr) {
|
|
210
|
+
if (ts.isIdentifier(expr)) return expr.text;
|
|
211
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
212
|
+
return calleeNameOf(expr.expression) + "." + expr.name.text;
|
|
213
|
+
}
|
|
214
|
+
return "<computed>";
|
|
215
|
+
}
|
|
216
|
+
function resolveCalleeFile(calleeName, importedNames) {
|
|
217
|
+
if (importedNames.has(calleeName)) return importedNames.get(calleeName);
|
|
218
|
+
const base = calleeName.split(".")[0];
|
|
219
|
+
if (importedNames.has(base)) return importedNames.get(base);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
function buildExportMap(files) {
|
|
223
|
+
const map = /* @__PURE__ */ new Map();
|
|
224
|
+
for (const { filePath, sourceText } of files) {
|
|
225
|
+
const sf = ts.createSourceFile(
|
|
226
|
+
filePath,
|
|
227
|
+
sourceText,
|
|
228
|
+
ts.ScriptTarget.Latest,
|
|
229
|
+
true
|
|
230
|
+
);
|
|
231
|
+
ts.forEachChild(sf, (node) => {
|
|
232
|
+
if (hasExportKeyword(node)) {
|
|
233
|
+
const name = declaredName(node);
|
|
234
|
+
if (name) map.set(name, filePath);
|
|
235
|
+
}
|
|
236
|
+
if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
237
|
+
for (const el of node.exportClause.elements) {
|
|
238
|
+
map.set(el.name.text, filePath);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return map;
|
|
244
|
+
}
|
|
245
|
+
function buildImportedNameMap(sourceFile, filePath, exportMap) {
|
|
246
|
+
const map = /* @__PURE__ */ new Map();
|
|
247
|
+
const fileDir = path2.dirname(filePath);
|
|
248
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
249
|
+
if (!ts.isImportDeclaration(node)) return;
|
|
250
|
+
const specifier = node.moduleSpecifier.text;
|
|
251
|
+
const resolvedPath = specifier.startsWith(".") ? toForwardSlash(path2.join(fileDir, specifier)) : null;
|
|
252
|
+
if (!node.importClause) return;
|
|
253
|
+
const clause = node.importClause;
|
|
254
|
+
if (clause.name) {
|
|
255
|
+
map.set(clause.name.text, resolvedPath ?? null);
|
|
256
|
+
}
|
|
257
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
258
|
+
for (const el of clause.namedBindings.elements) {
|
|
259
|
+
const resolvedViaExport = exportMap.get(el.name.text) ?? resolvedPath ?? null;
|
|
260
|
+
map.set(el.name.text, resolvedViaExport);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
|
|
264
|
+
map.set(clause.namedBindings.name.text, resolvedPath ?? null);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
return map;
|
|
268
|
+
}
|
|
269
|
+
function hasExportKeyword(node) {
|
|
270
|
+
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
271
|
+
return mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
272
|
+
}
|
|
273
|
+
function declaredName(node) {
|
|
274
|
+
if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) {
|
|
275
|
+
return node.name?.text ?? null;
|
|
276
|
+
}
|
|
277
|
+
if (ts.isVariableStatement(node)) {
|
|
278
|
+
const first = node.declarationList.declarations[0];
|
|
279
|
+
if (first && ts.isIdentifier(first.name)) return first.name.text;
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/languages/registry.ts
|
|
285
|
+
import * as path6 from "path";
|
|
286
|
+
|
|
287
|
+
// src/languages/typescript/adapter.ts
|
|
288
|
+
import * as path4 from "path";
|
|
289
|
+
|
|
290
|
+
// src/symbol-index/symbolExtractor.ts
|
|
291
|
+
import * as ts2 from "typescript";
|
|
292
|
+
import * as path3 from "path";
|
|
293
|
+
function extractFromSource(filePath, sourceText) {
|
|
294
|
+
const language = languageOf(filePath);
|
|
295
|
+
const scriptKind = language === "typescript" ? filePath.endsWith(".tsx") ? ts2.ScriptKind.TSX : ts2.ScriptKind.TS : ts2.ScriptKind.JS;
|
|
296
|
+
const sourceFile = ts2.createSourceFile(
|
|
297
|
+
filePath,
|
|
298
|
+
sourceText,
|
|
299
|
+
ts2.ScriptTarget.Latest,
|
|
300
|
+
/* setParentNodes */
|
|
301
|
+
true,
|
|
302
|
+
scriptKind
|
|
303
|
+
);
|
|
304
|
+
const lineCount = sourceText.split("\n").length;
|
|
305
|
+
const imports = [];
|
|
306
|
+
const exports = [];
|
|
307
|
+
const symbols = [];
|
|
308
|
+
const reExportSpecifiers = [];
|
|
309
|
+
const exportAllSpecifiers = [];
|
|
310
|
+
ts2.forEachChild(sourceFile, (node) => {
|
|
311
|
+
visitTopLevel(
|
|
312
|
+
node,
|
|
313
|
+
sourceFile,
|
|
314
|
+
filePath,
|
|
315
|
+
imports,
|
|
316
|
+
exports,
|
|
317
|
+
symbols,
|
|
318
|
+
reExportSpecifiers,
|
|
319
|
+
exportAllSpecifiers
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
language,
|
|
324
|
+
lineCount,
|
|
325
|
+
imports: dedup(imports),
|
|
326
|
+
exports: dedup(exports),
|
|
327
|
+
symbols,
|
|
328
|
+
reExportSpecifiers: dedup(reExportSpecifiers),
|
|
329
|
+
exportAllSpecifiers: dedup(exportAllSpecifiers)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function visitTopLevel(node, sourceFile, filePath, imports, exports, symbols, reExportSpecifiers, exportAllSpecifiers) {
|
|
333
|
+
if (ts2.isImportDeclaration(node)) {
|
|
334
|
+
const specifier = node.moduleSpecifier.text;
|
|
335
|
+
imports.push(specifier);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (ts2.isExportDeclaration(node)) {
|
|
339
|
+
const specifier = node.moduleSpecifier ? node.moduleSpecifier.text : null;
|
|
340
|
+
if (node.exportClause && ts2.isNamedExports(node.exportClause)) {
|
|
341
|
+
for (const el of node.exportClause.elements) {
|
|
342
|
+
exports.push(el.name.text);
|
|
343
|
+
}
|
|
344
|
+
if (specifier) reExportSpecifiers.push(specifier);
|
|
345
|
+
} else if (!node.exportClause && specifier) {
|
|
346
|
+
exportAllSpecifiers.push(specifier);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (ts2.isFunctionDeclaration(node) && node.name) {
|
|
351
|
+
const name = node.name.text;
|
|
352
|
+
const exp = hasExportKeyword2(node);
|
|
353
|
+
if (exp) exports.push(name);
|
|
354
|
+
symbols.push({
|
|
355
|
+
name,
|
|
356
|
+
kind: "function",
|
|
357
|
+
location: locationOf(node, sourceFile, filePath),
|
|
358
|
+
exported: exp,
|
|
359
|
+
signature: briefSignature(node, sourceFile)
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (ts2.isClassDeclaration(node) && node.name) {
|
|
364
|
+
const name = node.name.text;
|
|
365
|
+
const exp = hasExportKeyword2(node);
|
|
366
|
+
if (exp) exports.push(name);
|
|
367
|
+
symbols.push({
|
|
368
|
+
name,
|
|
369
|
+
kind: "class",
|
|
370
|
+
location: locationOf(node, sourceFile, filePath),
|
|
371
|
+
exported: exp,
|
|
372
|
+
signature: briefSignature(node, sourceFile)
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (ts2.isInterfaceDeclaration(node)) {
|
|
377
|
+
const name = node.name.text;
|
|
378
|
+
const exp = hasExportKeyword2(node);
|
|
379
|
+
if (exp) exports.push(name);
|
|
380
|
+
symbols.push({
|
|
381
|
+
name,
|
|
382
|
+
kind: "interface",
|
|
383
|
+
location: locationOf(node, sourceFile, filePath),
|
|
384
|
+
exported: exp,
|
|
385
|
+
signature: briefSignature(node, sourceFile)
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (ts2.isTypeAliasDeclaration(node)) {
|
|
390
|
+
const name = node.name.text;
|
|
391
|
+
const exp = hasExportKeyword2(node);
|
|
392
|
+
if (exp) exports.push(name);
|
|
393
|
+
symbols.push({
|
|
394
|
+
name,
|
|
395
|
+
kind: "type",
|
|
396
|
+
location: locationOf(node, sourceFile, filePath),
|
|
397
|
+
exported: exp,
|
|
398
|
+
signature: briefSignature(node, sourceFile)
|
|
399
|
+
});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (ts2.isEnumDeclaration(node)) {
|
|
403
|
+
const name = node.name.text;
|
|
404
|
+
const exp = hasExportKeyword2(node);
|
|
405
|
+
if (exp) exports.push(name);
|
|
406
|
+
symbols.push({
|
|
407
|
+
name,
|
|
408
|
+
kind: "enum",
|
|
409
|
+
location: locationOf(node, sourceFile, filePath),
|
|
410
|
+
exported: exp,
|
|
411
|
+
signature: briefSignature(node, sourceFile)
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (ts2.isVariableStatement(node)) {
|
|
416
|
+
const exp = hasExportKeyword2(node);
|
|
417
|
+
const isConst = !!(node.declarationList.flags & ts2.NodeFlags.Const);
|
|
418
|
+
const kind = isConst ? "const" : "variable";
|
|
419
|
+
for (const decl of node.declarationList.declarations) {
|
|
420
|
+
if (ts2.isIdentifier(decl.name)) {
|
|
421
|
+
const name = decl.name.text;
|
|
422
|
+
if (exp) exports.push(name);
|
|
423
|
+
symbols.push({
|
|
424
|
+
name,
|
|
425
|
+
kind,
|
|
426
|
+
location: locationOf(decl, sourceFile, filePath),
|
|
427
|
+
exported: exp,
|
|
428
|
+
signature: briefSignature(node, sourceFile)
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function hasExportKeyword2(node) {
|
|
436
|
+
const mods = ts2.canHaveModifiers(node) ? ts2.getModifiers(node) : void 0;
|
|
437
|
+
return mods?.some((m) => m.kind === ts2.SyntaxKind.ExportKeyword) ?? false;
|
|
438
|
+
}
|
|
439
|
+
function locationOf(node, sourceFile, filePath) {
|
|
440
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(
|
|
441
|
+
node.getStart(sourceFile)
|
|
442
|
+
);
|
|
443
|
+
return { file: filePath, line: line + 1 };
|
|
444
|
+
}
|
|
445
|
+
function briefSignature(node, sourceFile) {
|
|
446
|
+
const text = node.getText(sourceFile);
|
|
447
|
+
const firstLine = text.split("\n")[0].trim();
|
|
448
|
+
return firstLine.length > 120 ? firstLine.slice(0, 117) + "..." : firstLine;
|
|
449
|
+
}
|
|
450
|
+
function languageOf(filePath) {
|
|
451
|
+
const ext = path3.extname(filePath).toLowerCase();
|
|
452
|
+
if (ext === ".ts" || ext === ".tsx") return "typescript";
|
|
453
|
+
return "javascript";
|
|
454
|
+
}
|
|
455
|
+
function dedup(arr) {
|
|
456
|
+
return [...new Set(arr)];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/languages/typescript/adapter.ts
|
|
460
|
+
var TypeScriptAdapter = class {
|
|
461
|
+
extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
462
|
+
supportsCallGraph = true;
|
|
463
|
+
extractFromSource(filePath, sourceText) {
|
|
464
|
+
return extractFromSource(filePath, sourceText);
|
|
465
|
+
}
|
|
466
|
+
extractCallGraphEdges(files) {
|
|
467
|
+
return extractTypeScriptCallGraphEdges(files);
|
|
468
|
+
}
|
|
469
|
+
resolveImportToFile(specifier, importingFile, knownFiles) {
|
|
470
|
+
if (!specifier.startsWith(".")) return null;
|
|
471
|
+
const fromDir = toForwardSlash(path4.dirname(importingFile));
|
|
472
|
+
const base = toForwardSlash(path4.join(fromDir, specifier));
|
|
473
|
+
const knownSet = new Set(knownFiles);
|
|
474
|
+
if (knownSet.has(base)) return base;
|
|
475
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
476
|
+
const candidate = base + ext;
|
|
477
|
+
if (knownSet.has(candidate)) return candidate;
|
|
478
|
+
}
|
|
479
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
480
|
+
const candidate = base + "/index" + ext;
|
|
481
|
+
if (knownSet.has(candidate)) return candidate;
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// src/languages/python/adapter.ts
|
|
488
|
+
import { spawnSync } from "child_process";
|
|
489
|
+
import * as path5 from "path";
|
|
490
|
+
var EXTRACT_SYMBOLS_PY = `
|
|
491
|
+
import sys, ast, json
|
|
492
|
+
|
|
493
|
+
def main():
|
|
494
|
+
source = sys.stdin.read()
|
|
495
|
+
file_path = sys.argv[1] if len(sys.argv) > 1 else "<unknown>"
|
|
496
|
+
try:
|
|
497
|
+
tree = ast.parse(source, filename=file_path)
|
|
498
|
+
except SyntaxError as e:
|
|
499
|
+
sys.stdout.write(json.dumps({"error": "SyntaxError", "message": str(e), "lineno": e.lineno or 0}))
|
|
500
|
+
return
|
|
501
|
+
except Exception as e:
|
|
502
|
+
sys.stdout.write(json.dumps({"error": "ParseError", "message": str(e)}))
|
|
503
|
+
return
|
|
504
|
+
|
|
505
|
+
source_lines = source.splitlines()
|
|
506
|
+
|
|
507
|
+
def get_sig(lineno):
|
|
508
|
+
idx = lineno - 1
|
|
509
|
+
return source_lines[idx].strip()[:120] if 0 <= idx < len(source_lines) else ""
|
|
510
|
+
|
|
511
|
+
def decorator_names(dl):
|
|
512
|
+
names = []
|
|
513
|
+
for d in dl:
|
|
514
|
+
if isinstance(d, ast.Name): names.append(d.id)
|
|
515
|
+
elif isinstance(d, ast.Attribute): names.append(d.attr)
|
|
516
|
+
elif isinstance(d, ast.Call):
|
|
517
|
+
func = d.func
|
|
518
|
+
if isinstance(func, ast.Name): names.append(func.id)
|
|
519
|
+
elif isinstance(func, ast.Attribute): names.append(func.attr)
|
|
520
|
+
return names
|
|
521
|
+
|
|
522
|
+
dunder_all = None
|
|
523
|
+
for node in tree.body:
|
|
524
|
+
if isinstance(node, ast.Assign):
|
|
525
|
+
for t in node.targets:
|
|
526
|
+
if isinstance(t, ast.Name) and t.id == "__all__":
|
|
527
|
+
if isinstance(node.value, (ast.List, ast.Tuple)):
|
|
528
|
+
try:
|
|
529
|
+
dunder_all = [e.value for e in node.value.elts if isinstance(e, ast.Constant) and isinstance(e.value, str)]
|
|
530
|
+
except Exception:
|
|
531
|
+
dunder_all = None
|
|
532
|
+
|
|
533
|
+
def is_exported(name):
|
|
534
|
+
if dunder_all is not None: return name in dunder_all
|
|
535
|
+
return not name.startswith("_")
|
|
536
|
+
|
|
537
|
+
def is_all_caps(name):
|
|
538
|
+
alpha = [c for c in name if c.isalpha()]
|
|
539
|
+
return len(alpha) > 0 and all(c.isupper() for c in alpha)
|
|
540
|
+
|
|
541
|
+
def has_final(ann):
|
|
542
|
+
if ann is None: return False
|
|
543
|
+
if isinstance(ann, ast.Name) and ann.id == "Final": return True
|
|
544
|
+
if isinstance(ann, ast.Subscript):
|
|
545
|
+
v = ann.value
|
|
546
|
+
if isinstance(v, ast.Name) and v.id == "Final": return True
|
|
547
|
+
if isinstance(v, ast.Attribute) and v.attr == "Final": return True
|
|
548
|
+
if isinstance(ann, ast.Attribute) and ann.attr == "Final": return True
|
|
549
|
+
return False
|
|
550
|
+
|
|
551
|
+
symbols = []
|
|
552
|
+
for node in tree.body:
|
|
553
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
554
|
+
name = node.name
|
|
555
|
+
symbols.append({"name": name, "kind": "function", "line": node.lineno,
|
|
556
|
+
"end_line": getattr(node, "end_lineno", None),
|
|
557
|
+
"exported": is_exported(name), "signature": get_sig(node.lineno),
|
|
558
|
+
"decorators": decorator_names(node.decorator_list)})
|
|
559
|
+
elif isinstance(node, ast.ClassDef):
|
|
560
|
+
name = node.name
|
|
561
|
+
symbols.append({"name": name, "kind": "class", "line": node.lineno,
|
|
562
|
+
"end_line": getattr(node, "end_lineno", None),
|
|
563
|
+
"exported": is_exported(name), "signature": get_sig(node.lineno),
|
|
564
|
+
"decorators": decorator_names(node.decorator_list)})
|
|
565
|
+
elif isinstance(node, ast.Assign):
|
|
566
|
+
for t in node.targets:
|
|
567
|
+
if isinstance(t, ast.Name) and is_all_caps(t.id):
|
|
568
|
+
name = t.id
|
|
569
|
+
symbols.append({"name": name, "kind": "const", "line": node.lineno,
|
|
570
|
+
"end_line": getattr(node, "end_lineno", None),
|
|
571
|
+
"exported": is_exported(name), "signature": get_sig(node.lineno), "decorators": []})
|
|
572
|
+
elif isinstance(node, ast.AnnAssign):
|
|
573
|
+
if isinstance(node.target, ast.Name):
|
|
574
|
+
name = node.target.id
|
|
575
|
+
if has_final(node.annotation) or is_all_caps(name):
|
|
576
|
+
symbols.append({"name": name, "kind": "const", "line": node.lineno,
|
|
577
|
+
"end_line": getattr(node, "end_lineno", None),
|
|
578
|
+
"exported": is_exported(name), "signature": get_sig(node.lineno), "decorators": []})
|
|
579
|
+
sys.stdout.write(json.dumps({"symbols": symbols}))
|
|
580
|
+
|
|
581
|
+
main()
|
|
582
|
+
`;
|
|
583
|
+
var EXTRACT_IMPORTS_PY = `
|
|
584
|
+
import sys, ast, json
|
|
585
|
+
|
|
586
|
+
def main():
|
|
587
|
+
source = sys.stdin.read()
|
|
588
|
+
file_path = sys.argv[1] if len(sys.argv) > 1 else "<unknown>"
|
|
589
|
+
try:
|
|
590
|
+
tree = ast.parse(source, filename=file_path)
|
|
591
|
+
except SyntaxError as e:
|
|
592
|
+
sys.stdout.write(json.dumps({"error": "SyntaxError", "message": str(e), "lineno": e.lineno or 0}))
|
|
593
|
+
return
|
|
594
|
+
except Exception as e:
|
|
595
|
+
sys.stdout.write(json.dumps({"error": "ParseError", "message": str(e)}))
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
imports = []
|
|
599
|
+
for node in ast.walk(tree):
|
|
600
|
+
if isinstance(node, ast.Import):
|
|
601
|
+
for alias in node.names:
|
|
602
|
+
entry = {"form": "import-module-alias" if alias.asname else "import-module",
|
|
603
|
+
"module": alias.name, "names": [], "level": 0, "line": node.lineno}
|
|
604
|
+
if alias.asname: entry["alias"] = alias.asname
|
|
605
|
+
imports.append(entry)
|
|
606
|
+
elif isinstance(node, ast.ImportFrom):
|
|
607
|
+
module = node.module or ""
|
|
608
|
+
level = node.level or 0
|
|
609
|
+
names = [a.name for a in node.names]
|
|
610
|
+
single_alias = node.names[0].asname if len(node.names) == 1 and node.names[0].asname else None
|
|
611
|
+
entry = {"form": "relative-from-import" if level > 0 else "from-import",
|
|
612
|
+
"module": module, "names": names, "level": level, "line": node.lineno}
|
|
613
|
+
if single_alias: entry["alias"] = single_alias
|
|
614
|
+
imports.append(entry)
|
|
615
|
+
imports.sort(key=lambda x: x["line"])
|
|
616
|
+
sys.stdout.write(json.dumps({"imports": imports}))
|
|
617
|
+
|
|
618
|
+
main()
|
|
619
|
+
`;
|
|
620
|
+
var EXTRACT_CALLS_PY = `
|
|
621
|
+
import sys, ast, json
|
|
622
|
+
|
|
623
|
+
def main():
|
|
624
|
+
source = sys.stdin.read()
|
|
625
|
+
file_path = sys.argv[1] if len(sys.argv) > 1 else "<unknown>"
|
|
626
|
+
try:
|
|
627
|
+
tree = ast.parse(source, filename=file_path)
|
|
628
|
+
except SyntaxError as e:
|
|
629
|
+
sys.stdout.write(json.dumps({"error": "SyntaxError", "message": str(e), "lineno": e.lineno or 0}))
|
|
630
|
+
return
|
|
631
|
+
except Exception as e:
|
|
632
|
+
sys.stdout.write(json.dumps({"error": "ParseError", "message": str(e)}))
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
imports = []
|
|
636
|
+
definitions = []
|
|
637
|
+
|
|
638
|
+
for node in tree.body:
|
|
639
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
640
|
+
definitions.append({"name": node.name, "file": file_path, "line": node.lineno, "kind": "function"})
|
|
641
|
+
elif isinstance(node, ast.ClassDef):
|
|
642
|
+
definitions.append({"name": node.name, "file": file_path, "line": node.lineno, "kind": "class"})
|
|
643
|
+
for child in node.body:
|
|
644
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
645
|
+
definitions.append({"name": node.name + "." + child.name, "file": file_path, "line": child.lineno, "kind": "method"})
|
|
646
|
+
if isinstance(node, ast.Import):
|
|
647
|
+
for alias in node.names:
|
|
648
|
+
imports.append({"form": "import", "module": alias.name, "name": alias.asname or alias.name.split(".")[0]})
|
|
649
|
+
elif isinstance(node, ast.ImportFrom):
|
|
650
|
+
module = node.module or ""
|
|
651
|
+
for alias in node.names:
|
|
652
|
+
imports.append({"form": "from", "module": module, "level": node.level or 0, "imported": alias.name, "name": alias.asname or alias.name})
|
|
653
|
+
|
|
654
|
+
def callee_name(expr, class_name):
|
|
655
|
+
if isinstance(expr, ast.Name):
|
|
656
|
+
return expr.id
|
|
657
|
+
if isinstance(expr, ast.Attribute):
|
|
658
|
+
parts = []
|
|
659
|
+
cur = expr
|
|
660
|
+
while isinstance(cur, ast.Attribute):
|
|
661
|
+
parts.append(cur.attr)
|
|
662
|
+
cur = cur.value
|
|
663
|
+
if isinstance(cur, ast.Name):
|
|
664
|
+
if cur.id == "self" and class_name:
|
|
665
|
+
parts.append(class_name)
|
|
666
|
+
else:
|
|
667
|
+
parts.append(cur.id)
|
|
668
|
+
return ".".join(reversed(parts))
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
edges = []
|
|
672
|
+
|
|
673
|
+
class Visitor(ast.NodeVisitor):
|
|
674
|
+
def __init__(self):
|
|
675
|
+
self.class_name = None
|
|
676
|
+
self.caller = None
|
|
677
|
+
|
|
678
|
+
def visit_ClassDef(self, node):
|
|
679
|
+
old_class = self.class_name
|
|
680
|
+
self.class_name = node.name
|
|
681
|
+
for child in node.body:
|
|
682
|
+
self.visit(child)
|
|
683
|
+
self.class_name = old_class
|
|
684
|
+
|
|
685
|
+
def visit_FunctionDef(self, node):
|
|
686
|
+
self._visit_function(node)
|
|
687
|
+
|
|
688
|
+
def visit_AsyncFunctionDef(self, node):
|
|
689
|
+
self._visit_function(node)
|
|
690
|
+
|
|
691
|
+
def _visit_function(self, node):
|
|
692
|
+
old_caller = self.caller
|
|
693
|
+
name = self.class_name + "." + node.name if self.class_name else node.name
|
|
694
|
+
self.caller = {"file": file_path, "name": name, "line": node.lineno}
|
|
695
|
+
for child in node.body:
|
|
696
|
+
self.visit(child)
|
|
697
|
+
self.caller = old_caller
|
|
698
|
+
|
|
699
|
+
def visit_Call(self, node):
|
|
700
|
+
if self.caller:
|
|
701
|
+
name = callee_name(node.func, self.class_name)
|
|
702
|
+
if name:
|
|
703
|
+
edges.append({"caller": self.caller, "calleeName": name, "callLine": node.lineno})
|
|
704
|
+
self.generic_visit(node)
|
|
705
|
+
|
|
706
|
+
Visitor().visit(tree)
|
|
707
|
+
sys.stdout.write(json.dumps({"definitions": definitions, "imports": imports, "edges": edges}))
|
|
708
|
+
|
|
709
|
+
main()
|
|
710
|
+
`;
|
|
711
|
+
var PYTHON_TIMEOUT_MS = 15e3;
|
|
712
|
+
function findPythonCmd() {
|
|
713
|
+
for (const cmd of ["python", "python3"]) {
|
|
714
|
+
const probe = spawnSync(cmd, ["--version"], { encoding: "utf8", shell: false, timeout: 5e3 });
|
|
715
|
+
if (!probe.error && probe.status === 0) return cmd;
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
var _pythonCmd = void 0;
|
|
720
|
+
function getPythonCmd() {
|
|
721
|
+
if (_pythonCmd === void 0) _pythonCmd = findPythonCmd();
|
|
722
|
+
return _pythonCmd;
|
|
723
|
+
}
|
|
724
|
+
function runPythonScript(script, filePath, sourceText) {
|
|
725
|
+
const cmd = getPythonCmd();
|
|
726
|
+
if (!cmd) {
|
|
727
|
+
return { stdout: "", stderr: "", error: "Python interpreter not found on PATH (tried python, python3)." };
|
|
728
|
+
}
|
|
729
|
+
const result = spawnSync(cmd, ["-c", script, filePath], {
|
|
730
|
+
input: sourceText,
|
|
731
|
+
encoding: "utf8",
|
|
732
|
+
shell: false,
|
|
733
|
+
timeout: PYTHON_TIMEOUT_MS
|
|
734
|
+
});
|
|
735
|
+
if (result.error) {
|
|
736
|
+
const e = result.error;
|
|
737
|
+
return { stdout: "", stderr: "", error: `Python subprocess error: ${e.message}` };
|
|
738
|
+
}
|
|
739
|
+
if (result.status === null) {
|
|
740
|
+
return { stdout: "", stderr: "", error: `Python subprocess timed out after ${PYTHON_TIMEOUT_MS}ms` };
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
stdout: result.stdout ?? "",
|
|
744
|
+
stderr: result.stderr ?? "",
|
|
745
|
+
error: null
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function parseJsonOutput(raw, filePath, context) {
|
|
749
|
+
const trimmed = raw.trim();
|
|
750
|
+
if (!trimmed) {
|
|
751
|
+
return { data: null, warning: `python-extraction: no output for ${context} from ${filePath}` };
|
|
752
|
+
}
|
|
753
|
+
let parsed;
|
|
754
|
+
try {
|
|
755
|
+
parsed = JSON.parse(trimmed);
|
|
756
|
+
} catch {
|
|
757
|
+
return { data: null, warning: `python-extraction: could not parse ${context} output for ${filePath}` };
|
|
758
|
+
}
|
|
759
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
760
|
+
return { data: null, warning: `python-extraction: unexpected ${context} output shape for ${filePath}` };
|
|
761
|
+
}
|
|
762
|
+
const obj = parsed;
|
|
763
|
+
if ("error" in obj) {
|
|
764
|
+
const msg = typeof obj["message"] === "string" ? obj["message"] : String(obj["error"]);
|
|
765
|
+
return { data: null, warning: `python-extraction: ${msg} in ${filePath}` };
|
|
766
|
+
}
|
|
767
|
+
return { data: obj, warning: null };
|
|
768
|
+
}
|
|
769
|
+
function mapPythonKind(kind) {
|
|
770
|
+
if (kind === "function") return "function";
|
|
771
|
+
if (kind === "class") return "class";
|
|
772
|
+
if (kind === "const") return "const";
|
|
773
|
+
return "variable";
|
|
774
|
+
}
|
|
775
|
+
function buildImportSpecifier(imp) {
|
|
776
|
+
if (imp.level > 0) {
|
|
777
|
+
const dots = ".".repeat(imp.level);
|
|
778
|
+
const mod = imp.module ? `${dots}${imp.module}` : dots;
|
|
779
|
+
return imp.names.length > 0 ? `from ${mod} import ${imp.names.join(", ")}` : mod;
|
|
780
|
+
}
|
|
781
|
+
if (imp.form === "import-module" || imp.form === "import-module-alias") {
|
|
782
|
+
return imp.module;
|
|
783
|
+
}
|
|
784
|
+
return imp.module;
|
|
785
|
+
}
|
|
786
|
+
function dedup2(arr) {
|
|
787
|
+
return [...new Set(arr)];
|
|
788
|
+
}
|
|
789
|
+
function buildModulePathMap(knownFiles) {
|
|
790
|
+
const map = /* @__PURE__ */ new Map();
|
|
791
|
+
for (const f of knownFiles) {
|
|
792
|
+
const norm = f.replace(/\\/g, "/");
|
|
793
|
+
const withoutExt = norm.endsWith(".py") ? norm.slice(0, -3) : norm.endsWith("/__init__.py") ? norm.slice(0, -12) : null;
|
|
794
|
+
if (withoutExt) {
|
|
795
|
+
const parts = withoutExt.split("/");
|
|
796
|
+
for (let start = parts.length - 1; start >= 0; start--) {
|
|
797
|
+
const key = parts.slice(start).join(".");
|
|
798
|
+
if (!map.has(key)) map.set(key, norm);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return map;
|
|
803
|
+
}
|
|
804
|
+
function resolveAbsoluteImport(module, knownSet, modulePathMap) {
|
|
805
|
+
if (!module) return null;
|
|
806
|
+
const direct = modulePathMap.get(module);
|
|
807
|
+
if (direct) return direct;
|
|
808
|
+
const asPath = module.replace(/\./g, "/");
|
|
809
|
+
for (const candidate of [asPath + ".py", asPath + "/__init__.py"]) {
|
|
810
|
+
if (knownSet.has(candidate)) return candidate;
|
|
811
|
+
for (const f of knownSet) {
|
|
812
|
+
if (f.endsWith("/" + candidate) || f === candidate) return f;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
function resolveRelativeImport(imp, importingFile, knownSet) {
|
|
818
|
+
const norm = importingFile.replace(/\\/g, "/");
|
|
819
|
+
const dir = path5.posix.dirname(norm);
|
|
820
|
+
const parts = dir.split("/").filter((p) => p.length > 0);
|
|
821
|
+
const upCount = imp.level - 1;
|
|
822
|
+
if (upCount > parts.length) return null;
|
|
823
|
+
const baseParts = upCount > 0 ? parts.slice(0, parts.length - upCount) : parts;
|
|
824
|
+
const baseDir = baseParts.length > 0 ? baseParts.join("/") : ".";
|
|
825
|
+
if (!imp.module) {
|
|
826
|
+
if (imp.names.length === 0 || imp.names[0] === "*") return null;
|
|
827
|
+
const name = imp.names[0];
|
|
828
|
+
for (const c of [baseDir + "/" + name + ".py", baseDir + "/" + name + "/__init__.py"]) {
|
|
829
|
+
if (knownSet.has(c)) return c;
|
|
830
|
+
}
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
const moduleParts = imp.module.split(".");
|
|
834
|
+
const modulePath = moduleParts.join("/");
|
|
835
|
+
for (const c of [baseDir + "/" + modulePath + ".py", baseDir + "/" + modulePath + "/__init__.py"]) {
|
|
836
|
+
if (knownSet.has(c)) return c;
|
|
837
|
+
}
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
function specifierToPythonImport(specifier) {
|
|
841
|
+
const relMatch = specifier.match(/^from (\.+)([^\s]*)\s+import/);
|
|
842
|
+
if (relMatch) {
|
|
843
|
+
const level = relMatch[1].length;
|
|
844
|
+
const module = relMatch[2];
|
|
845
|
+
return { form: "relative-from-import", module, names: [], level, line: 0 };
|
|
846
|
+
}
|
|
847
|
+
const dotMatch = specifier.match(/^(\.+)(.*)$/);
|
|
848
|
+
if (dotMatch) {
|
|
849
|
+
return { form: "relative-from-import", module: dotMatch[2], names: [], level: dotMatch[1].length, line: 0 };
|
|
850
|
+
}
|
|
851
|
+
return { form: "from-import", module: specifier, names: [], level: 0, line: 0 };
|
|
852
|
+
}
|
|
853
|
+
function resolvePythonImportToFile(imp, importingFile, knownFiles) {
|
|
854
|
+
const knownSet = new Set(knownFiles);
|
|
855
|
+
const modulePathMap = buildModulePathMap(knownFiles);
|
|
856
|
+
if (imp.level > 0) {
|
|
857
|
+
return resolveRelativeImport(imp, importingFile, knownSet);
|
|
858
|
+
}
|
|
859
|
+
return resolveAbsoluteImport(imp.module, knownSet, modulePathMap);
|
|
860
|
+
}
|
|
861
|
+
var PythonAdapter = class {
|
|
862
|
+
extensions = [".py"];
|
|
863
|
+
supportsCallGraph = true;
|
|
864
|
+
extractFromSource(filePath, sourceText) {
|
|
865
|
+
const lineCount = sourceText.split("\n").length;
|
|
866
|
+
const warnings = [];
|
|
867
|
+
const symbols = [];
|
|
868
|
+
const imports = [];
|
|
869
|
+
const exports = [];
|
|
870
|
+
const symResult = runPythonScript(EXTRACT_SYMBOLS_PY, filePath, sourceText);
|
|
871
|
+
if (symResult.error) {
|
|
872
|
+
warnings.push(`python-extraction: ${symResult.error}`);
|
|
873
|
+
} else {
|
|
874
|
+
const parsed = parseJsonOutput(symResult.stdout, filePath, "symbols");
|
|
875
|
+
if (parsed.warning) {
|
|
876
|
+
warnings.push(parsed.warning);
|
|
877
|
+
} else if (parsed.data) {
|
|
878
|
+
const rawSymbols = Array.isArray(parsed.data["symbols"]) ? parsed.data["symbols"] : [];
|
|
879
|
+
for (const s of rawSymbols) {
|
|
880
|
+
if (!s.name || !s.kind) continue;
|
|
881
|
+
if (s.exported) exports.push(s.name);
|
|
882
|
+
symbols.push({
|
|
883
|
+
name: s.name,
|
|
884
|
+
kind: mapPythonKind(s.kind),
|
|
885
|
+
location: { file: filePath, line: s.line },
|
|
886
|
+
exported: s.exported,
|
|
887
|
+
signature: s.signature
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const impResult = runPythonScript(EXTRACT_IMPORTS_PY, filePath, sourceText);
|
|
893
|
+
if (impResult.error) {
|
|
894
|
+
warnings.push(`python-extraction: ${impResult.error}`);
|
|
895
|
+
} else {
|
|
896
|
+
const parsed = parseJsonOutput(impResult.stdout, filePath, "imports");
|
|
897
|
+
if (parsed.warning) {
|
|
898
|
+
warnings.push(parsed.warning);
|
|
899
|
+
} else if (parsed.data) {
|
|
900
|
+
const rawImports = Array.isArray(parsed.data["imports"]) ? parsed.data["imports"] : [];
|
|
901
|
+
for (const imp of rawImports) {
|
|
902
|
+
const spec = buildImportSpecifier(imp);
|
|
903
|
+
if (spec) imports.push(spec);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
language: "python",
|
|
909
|
+
lineCount,
|
|
910
|
+
imports: dedup2(imports),
|
|
911
|
+
exports: dedup2(exports),
|
|
912
|
+
symbols,
|
|
913
|
+
reExportSpecifiers: [],
|
|
914
|
+
exportAllSpecifiers: []
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
resolveImportToFile(specifier, importingFile, knownFiles) {
|
|
918
|
+
const imp = specifierToPythonImport(specifier);
|
|
919
|
+
return resolvePythonImportToFile(imp, importingFile, knownFiles);
|
|
920
|
+
}
|
|
921
|
+
extractCallGraphEdges(files) {
|
|
922
|
+
const extractions = /* @__PURE__ */ new Map();
|
|
923
|
+
const definitionsByName = /* @__PURE__ */ new Map();
|
|
924
|
+
const knownFiles = files.map((f) => f.filePath);
|
|
925
|
+
for (const file of files) {
|
|
926
|
+
const result = runPythonScript(EXTRACT_CALLS_PY, file.filePath, file.sourceText);
|
|
927
|
+
if (result.error) continue;
|
|
928
|
+
const parsed = parseJsonOutput(result.stdout, file.filePath, "calls");
|
|
929
|
+
if (!parsed.data || parsed.warning) continue;
|
|
930
|
+
const extraction = {
|
|
931
|
+
definitions: Array.isArray(parsed.data["definitions"]) ? parsed.data["definitions"] : [],
|
|
932
|
+
imports: Array.isArray(parsed.data["imports"]) ? parsed.data["imports"] : [],
|
|
933
|
+
edges: Array.isArray(parsed.data["edges"]) ? parsed.data["edges"] : []
|
|
934
|
+
};
|
|
935
|
+
extractions.set(file.filePath, extraction);
|
|
936
|
+
for (const definition of extraction.definitions) {
|
|
937
|
+
if (!definitionsByName.has(definition.name)) {
|
|
938
|
+
definitionsByName.set(definition.name, definition);
|
|
939
|
+
}
|
|
940
|
+
const shortName = definition.kind === "method" ? null : definition.name.split(".").at(-1);
|
|
941
|
+
if (shortName && !definitionsByName.has(shortName)) {
|
|
942
|
+
definitionsByName.set(shortName, definition);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const edges = [];
|
|
947
|
+
for (const [filePath, extraction] of extractions) {
|
|
948
|
+
const importMap = buildPythonCallImportMap(extraction.imports, filePath, knownFiles);
|
|
949
|
+
for (const rawEdge of extraction.edges) {
|
|
950
|
+
edges.push({
|
|
951
|
+
caller: rawEdge.caller,
|
|
952
|
+
callee: {
|
|
953
|
+
file: resolvePythonCalleeFile(rawEdge.calleeName, importMap, definitionsByName),
|
|
954
|
+
name: rawEdge.calleeName
|
|
955
|
+
},
|
|
956
|
+
callLine: rawEdge.callLine
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return dedupCallEdges(edges);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
function buildPythonCallImportMap(imports, importingFile, knownFiles) {
|
|
964
|
+
const map = /* @__PURE__ */ new Map();
|
|
965
|
+
for (const imp of imports) {
|
|
966
|
+
if (imp.form === "import") {
|
|
967
|
+
const file2 = resolvePythonImportToFile(
|
|
968
|
+
{ form: "import-module", module: imp.module, names: [], level: 0, line: 0 },
|
|
969
|
+
importingFile,
|
|
970
|
+
knownFiles
|
|
971
|
+
);
|
|
972
|
+
map.set(imp.name, file2);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const file = resolvePythonImportToFile(
|
|
976
|
+
{
|
|
977
|
+
form: (imp.level ?? 0) > 0 ? "relative-from-import" : "from-import",
|
|
978
|
+
module: imp.module,
|
|
979
|
+
names: imp.imported ? [imp.imported] : [],
|
|
980
|
+
level: imp.level ?? 0,
|
|
981
|
+
line: 0
|
|
982
|
+
},
|
|
983
|
+
importingFile,
|
|
984
|
+
knownFiles
|
|
985
|
+
);
|
|
986
|
+
map.set(imp.name, file);
|
|
987
|
+
}
|
|
988
|
+
return map;
|
|
989
|
+
}
|
|
990
|
+
function resolvePythonCalleeFile(calleeName, importMap, definitionsByName) {
|
|
991
|
+
const directDefinition = definitionsByName.get(calleeName);
|
|
992
|
+
if (directDefinition) return directDefinition.file;
|
|
993
|
+
const parts = calleeName.split(".");
|
|
994
|
+
const base = parts[0];
|
|
995
|
+
if (importMap.has(base)) return importMap.get(base) ?? null;
|
|
996
|
+
if (parts.length === 1) {
|
|
997
|
+
const short = parts[0];
|
|
998
|
+
const shortDefinition = definitionsByName.get(short);
|
|
999
|
+
if (shortDefinition) return shortDefinition.file;
|
|
1000
|
+
}
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
function dedupCallEdges(edges) {
|
|
1004
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1005
|
+
return edges.filter((edge) => {
|
|
1006
|
+
const key = [
|
|
1007
|
+
edge.caller.file,
|
|
1008
|
+
edge.caller.name,
|
|
1009
|
+
edge.caller.line,
|
|
1010
|
+
edge.callee.file ?? "",
|
|
1011
|
+
edge.callee.name,
|
|
1012
|
+
edge.callLine
|
|
1013
|
+
].join("\0");
|
|
1014
|
+
if (seen.has(key)) return false;
|
|
1015
|
+
seen.add(key);
|
|
1016
|
+
return true;
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/languages/registry.ts
|
|
1021
|
+
var LanguageRegistry = class {
|
|
1022
|
+
adapters = [];
|
|
1023
|
+
register(adapter) {
|
|
1024
|
+
this.adapters.push(adapter);
|
|
1025
|
+
}
|
|
1026
|
+
adapterForFile(filePath) {
|
|
1027
|
+
const ext = path6.extname(filePath).toLowerCase();
|
|
1028
|
+
for (const adapter of this.adapters) {
|
|
1029
|
+
if (adapter.extensions.includes(ext)) return adapter;
|
|
1030
|
+
}
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
supportsFile(filename) {
|
|
1034
|
+
return this.adapterForFile(filename) !== null;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
function createDefaultRegistry() {
|
|
1038
|
+
const registry = new LanguageRegistry();
|
|
1039
|
+
registry.register(new TypeScriptAdapter());
|
|
1040
|
+
registry.register(new PythonAdapter());
|
|
1041
|
+
return registry;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/indexing/discoverSourceFiles.ts
|
|
1045
|
+
import * as fs from "fs";
|
|
1046
|
+
import * as path7 from "path";
|
|
1047
|
+
var DEFAULT_IGNORED_DIRECTORY_NAMES = [
|
|
1048
|
+
"node_modules",
|
|
1049
|
+
".next",
|
|
1050
|
+
"dist",
|
|
1051
|
+
"build",
|
|
1052
|
+
"coverage",
|
|
1053
|
+
"playwright-report",
|
|
1054
|
+
"test-results",
|
|
1055
|
+
"output",
|
|
1056
|
+
"out",
|
|
1057
|
+
".cache",
|
|
1058
|
+
".turbo",
|
|
1059
|
+
".vercel",
|
|
1060
|
+
".git",
|
|
1061
|
+
".pytest_cache",
|
|
1062
|
+
"__pycache__",
|
|
1063
|
+
".venv",
|
|
1064
|
+
"venv"
|
|
1065
|
+
];
|
|
1066
|
+
var DEFAULT_FILE_EXCLUDE_PATTERNS = [".d.ts", ".test.", ".spec."];
|
|
1067
|
+
var SAMPLE_LIMIT = 20;
|
|
1068
|
+
var LARGEST_FILE_LIMIT = 10;
|
|
1069
|
+
function discoverSourceFiles(options) {
|
|
1070
|
+
const registry = options.registry ?? createDefaultRegistry();
|
|
1071
|
+
const state = {
|
|
1072
|
+
repoRoot: options.repoRoot,
|
|
1073
|
+
registry,
|
|
1074
|
+
defaultIgnoredDirectoryNames: new Set(DEFAULT_IGNORED_DIRECTORY_NAMES.map((name) => name.toLowerCase())),
|
|
1075
|
+
userExcludeRules: normalizeExcludeRules(options.userExcludes ?? []),
|
|
1076
|
+
userExcludes: (options.userExcludes ?? []).map(normalizePathInput),
|
|
1077
|
+
startTime: Date.now(),
|
|
1078
|
+
lastProgressTime: 0,
|
|
1079
|
+
onProgress: options.onProgress,
|
|
1080
|
+
result: {
|
|
1081
|
+
files: [],
|
|
1082
|
+
defaultIgnoredDirectoryNames: [...DEFAULT_IGNORED_DIRECTORY_NAMES],
|
|
1083
|
+
userExcludes: (options.userExcludes ?? []).map(normalizePathInput),
|
|
1084
|
+
totalFilesDiscovered: 0,
|
|
1085
|
+
totalFilesEligibleForIndexing: 0,
|
|
1086
|
+
totalFilesSkipped: 0,
|
|
1087
|
+
skippedByDefaultIgnore: 0,
|
|
1088
|
+
skippedByUserExclude: 0,
|
|
1089
|
+
skippedByFilePattern: 0,
|
|
1090
|
+
skippedUnsupportedFiles: 0,
|
|
1091
|
+
languageCounts: {},
|
|
1092
|
+
largestFiles: [],
|
|
1093
|
+
sampleIndexedFiles: [],
|
|
1094
|
+
sampleSkippedFiles: []
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
emitProgress(state, "scan-start", "Scanning source roots");
|
|
1098
|
+
for (const root of options.sourceRoots) {
|
|
1099
|
+
emitProgress(state, "scan-source-root", `Scanning ${root}`, root);
|
|
1100
|
+
collectFiles(path7.resolve(options.repoRoot, root), state, root);
|
|
1101
|
+
}
|
|
1102
|
+
emitProgress(state, "scan-complete", "Source scan completed");
|
|
1103
|
+
state.result.largestFiles.sort((a, b) => b.sizeBytes - a.sizeBytes);
|
|
1104
|
+
return state.result;
|
|
1105
|
+
}
|
|
1106
|
+
function collectFiles(dir, state, currentSourceRoot) {
|
|
1107
|
+
let entries;
|
|
1108
|
+
try {
|
|
1109
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1110
|
+
} catch {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
for (const entry of entries) {
|
|
1114
|
+
const absPath = path7.join(dir, entry.name);
|
|
1115
|
+
const relPath = toForwardSlash(path7.relative(state.repoRoot, absPath));
|
|
1116
|
+
if (entry.isDirectory()) {
|
|
1117
|
+
const userExclude2 = matchesUserExclude(relPath, entry.name, state.userExcludeRules);
|
|
1118
|
+
if (userExclude2) {
|
|
1119
|
+
recordSkipped(state, relPath, "user-exclude");
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
if (state.defaultIgnoredDirectoryNames.has(entry.name.toLowerCase())) {
|
|
1123
|
+
recordSkipped(state, relPath, "default-ignore");
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
collectFiles(absPath, state, currentSourceRoot);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (!entry.isFile()) continue;
|
|
1130
|
+
const userExclude = matchesUserExclude(relPath, entry.name, state.userExcludeRules);
|
|
1131
|
+
if (userExclude) {
|
|
1132
|
+
recordSkipped(state, relPath, "user-exclude");
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
state.result.totalFilesDiscovered += 1;
|
|
1136
|
+
let stat;
|
|
1137
|
+
try {
|
|
1138
|
+
stat = fs.statSync(absPath);
|
|
1139
|
+
} catch {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
if (shouldExcludeByFilePattern(relPath)) {
|
|
1143
|
+
recordSkipped(state, relPath, "file-pattern");
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (!state.registry.supportsFile(entry.name)) {
|
|
1147
|
+
recordSkipped(state, relPath, "unsupported");
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
state.result.files.push({ relPath, absPath, sizeBytes: stat.size });
|
|
1151
|
+
state.result.totalFilesEligibleForIndexing += 1;
|
|
1152
|
+
incrementLanguageCount(state.result.languageCounts, relPath);
|
|
1153
|
+
pushSample(state.result.sampleIndexedFiles, relPath);
|
|
1154
|
+
trackLargestFile(state.result.largestFiles, { path: relPath, sizeBytes: stat.size });
|
|
1155
|
+
maybeEmitProgress(state, currentSourceRoot);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
function recordSkipped(state, relPath, reason) {
|
|
1159
|
+
state.result.totalFilesSkipped += 1;
|
|
1160
|
+
if (reason === "default-ignore") state.result.skippedByDefaultIgnore += 1;
|
|
1161
|
+
if (reason === "user-exclude") state.result.skippedByUserExclude += 1;
|
|
1162
|
+
if (reason === "file-pattern") state.result.skippedByFilePattern += 1;
|
|
1163
|
+
if (reason === "unsupported") state.result.skippedUnsupportedFiles += 1;
|
|
1164
|
+
pushSample(state.result.sampleSkippedFiles, { path: relPath, reason });
|
|
1165
|
+
}
|
|
1166
|
+
function shouldExcludeByFilePattern(relPath) {
|
|
1167
|
+
return DEFAULT_FILE_EXCLUDE_PATTERNS.some((pattern) => relPath.includes(pattern));
|
|
1168
|
+
}
|
|
1169
|
+
function normalizeExcludeRules(values) {
|
|
1170
|
+
return values.map(normalizePathInput).filter((value) => value.length > 0).map((value) => ({
|
|
1171
|
+
normalized: value,
|
|
1172
|
+
isPath: value.includes("/")
|
|
1173
|
+
}));
|
|
1174
|
+
}
|
|
1175
|
+
function normalizePathInput(value) {
|
|
1176
|
+
return toForwardSlash(value.trim()).replace(/^\/+|\/+$/g, "");
|
|
1177
|
+
}
|
|
1178
|
+
function matchesUserExclude(relPath, basename2, rules) {
|
|
1179
|
+
const normalizedRelPath = normalizePathInput(relPath);
|
|
1180
|
+
const segments = normalizedRelPath.split("/");
|
|
1181
|
+
return rules.some((rule) => {
|
|
1182
|
+
if (rule.isPath) {
|
|
1183
|
+
return normalizedRelPath === rule.normalized || normalizedRelPath.startsWith(`${rule.normalized}/`);
|
|
1184
|
+
}
|
|
1185
|
+
return basename2 === rule.normalized || segments.includes(rule.normalized);
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
function incrementLanguageCount(languageCounts, relPath) {
|
|
1189
|
+
const ext = path7.extname(relPath).toLowerCase();
|
|
1190
|
+
const language = ext === ".py" ? "python" : ext === ".ts" || ext === ".tsx" ? "typescript" : ext === ".js" || ext === ".jsx" ? "javascript" : "unknown";
|
|
1191
|
+
languageCounts[language] = (languageCounts[language] ?? 0) + 1;
|
|
1192
|
+
}
|
|
1193
|
+
function pushSample(samples, sample) {
|
|
1194
|
+
if (samples.length < SAMPLE_LIMIT) samples.push(sample);
|
|
1195
|
+
}
|
|
1196
|
+
function trackLargestFile(samples, sample) {
|
|
1197
|
+
samples.push(sample);
|
|
1198
|
+
samples.sort((a, b) => b.sizeBytes - a.sizeBytes);
|
|
1199
|
+
if (samples.length > LARGEST_FILE_LIMIT) samples.pop();
|
|
1200
|
+
}
|
|
1201
|
+
function maybeEmitProgress(state, currentSourceRoot) {
|
|
1202
|
+
const now = Date.now();
|
|
1203
|
+
if (now - state.lastProgressTime < 1e3) return;
|
|
1204
|
+
state.lastProgressTime = now;
|
|
1205
|
+
emitProgress(state, "scan-progress", "Scanning source files", currentSourceRoot);
|
|
1206
|
+
}
|
|
1207
|
+
function emitProgress(state, phase, message, currentSourceRoot) {
|
|
1208
|
+
if (!state.onProgress) return;
|
|
1209
|
+
state.onProgress({
|
|
1210
|
+
phase,
|
|
1211
|
+
message,
|
|
1212
|
+
currentSourceRoot,
|
|
1213
|
+
elapsedMs: Date.now() - state.startTime,
|
|
1214
|
+
filesDiscovered: state.result.totalFilesDiscovered,
|
|
1215
|
+
filesEligible: state.result.totalFilesEligibleForIndexing,
|
|
1216
|
+
filesSkipped: state.result.totalFilesSkipped
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// src/symbol-index/builder.ts
|
|
1221
|
+
function buildIndex(config) {
|
|
1222
|
+
const startTime = Date.now();
|
|
1223
|
+
const buildTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
1224
|
+
const registry = createDefaultRegistry();
|
|
1225
|
+
const discovery = discoverSourceFiles({
|
|
1226
|
+
repoRoot: config.repoRoot,
|
|
1227
|
+
sourceRoots: config.sourceRoots,
|
|
1228
|
+
userExcludes: config.excludePatterns,
|
|
1229
|
+
registry,
|
|
1230
|
+
onProgress: config.onProgress
|
|
1231
|
+
});
|
|
1232
|
+
const sourceFiles = discovery.files;
|
|
1233
|
+
const callGraphInputsByAdapter = /* @__PURE__ */ new Map();
|
|
1234
|
+
const summaries = [];
|
|
1235
|
+
const rawExtractions = [];
|
|
1236
|
+
emitBuildProgress(config, "index-start", "Indexing started", startTime, 0, sourceFiles.length);
|
|
1237
|
+
let filesIndexed = 0;
|
|
1238
|
+
let lastProgressTime = 0;
|
|
1239
|
+
for (const { relPath, absPath } of sourceFiles) {
|
|
1240
|
+
let sourceText;
|
|
1241
|
+
try {
|
|
1242
|
+
sourceText = fs2.readFileSync(absPath, "utf-8");
|
|
1243
|
+
} catch {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
const adapter = registry.adapterForFile(relPath);
|
|
1247
|
+
const result = adapter.extractFromSource(relPath, sourceText);
|
|
1248
|
+
rawExtractions.push({ relPath, extraction: result });
|
|
1249
|
+
summaries.push({
|
|
1250
|
+
path: relPath,
|
|
1251
|
+
language: result.language,
|
|
1252
|
+
lineCount: result.lineCount,
|
|
1253
|
+
imports: result.imports,
|
|
1254
|
+
exports: result.exports,
|
|
1255
|
+
symbols: result.symbols,
|
|
1256
|
+
hasCallGraphEntries: false
|
|
1257
|
+
// updated below if call graph is built
|
|
1258
|
+
});
|
|
1259
|
+
if (config.buildCallGraph && adapter.supportsCallGraph && adapter.extractCallGraphEdges) {
|
|
1260
|
+
const inputs = callGraphInputsByAdapter.get(adapter) ?? [];
|
|
1261
|
+
inputs.push({ filePath: relPath, sourceText });
|
|
1262
|
+
callGraphInputsByAdapter.set(adapter, inputs);
|
|
1263
|
+
}
|
|
1264
|
+
filesIndexed += 1;
|
|
1265
|
+
const now = Date.now();
|
|
1266
|
+
if (now - lastProgressTime >= 1e3) {
|
|
1267
|
+
lastProgressTime = now;
|
|
1268
|
+
emitBuildProgress(config, "index-progress", "Indexing source files", startTime, filesIndexed, sourceFiles.length);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
emitBuildProgress(config, "index-complete", "Indexing completed", startTime, filesIndexed, sourceFiles.length);
|
|
1272
|
+
let callGraph = null;
|
|
1273
|
+
if (config.buildCallGraph && callGraphInputsByAdapter.size > 0) {
|
|
1274
|
+
emitBuildProgress(config, "call-graph-start", "Call graph building started", startTime, filesIndexed, sourceFiles.length);
|
|
1275
|
+
const edges = [];
|
|
1276
|
+
for (const [adapter, inputs] of callGraphInputsByAdapter) {
|
|
1277
|
+
edges.push(...adapter.extractCallGraphEdges(inputs));
|
|
1278
|
+
}
|
|
1279
|
+
callGraph = createCallGraph(edges);
|
|
1280
|
+
const filesWithEdges = /* @__PURE__ */ new Set();
|
|
1281
|
+
for (const edge of callGraph.edges) {
|
|
1282
|
+
filesWithEdges.add(edge.caller.file);
|
|
1283
|
+
if (edge.callee.file) filesWithEdges.add(edge.callee.file);
|
|
1284
|
+
}
|
|
1285
|
+
for (const summary of summaries) {
|
|
1286
|
+
if (filesWithEdges.has(summary.path)) {
|
|
1287
|
+
summary.hasCallGraphEntries = true;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
emitBuildProgress(config, "call-graph-complete", "Call graph building completed", startTime, filesIndexed, sourceFiles.length);
|
|
1291
|
+
}
|
|
1292
|
+
const indexedFileSet = new Set(summaries.map((s) => s.path));
|
|
1293
|
+
const graph = buildGraphSection(rawExtractions, indexedFileSet, registry);
|
|
1294
|
+
const symbolCount = summaries.reduce((n, f) => n + f.symbols.length, 0);
|
|
1295
|
+
const index = {
|
|
1296
|
+
schemaVersion: SCHEMA_VERSION,
|
|
1297
|
+
buildTime,
|
|
1298
|
+
repoRoot: config.repoRoot,
|
|
1299
|
+
sourceRoots: config.sourceRoots,
|
|
1300
|
+
fileCount: summaries.length,
|
|
1301
|
+
symbolCount,
|
|
1302
|
+
files: summaries,
|
|
1303
|
+
graph
|
|
1304
|
+
// callGraphArtifact set by writer after writing the artifact
|
|
1305
|
+
};
|
|
1306
|
+
return { index, callGraph, discovery };
|
|
1307
|
+
}
|
|
1308
|
+
function buildGraphSection(rawExtractions, indexedFileSet, registry) {
|
|
1309
|
+
const fileDeps = [];
|
|
1310
|
+
const symbols = [];
|
|
1311
|
+
const knownFiles = [...indexedFileSet];
|
|
1312
|
+
for (const { relPath, extraction } of rawExtractions) {
|
|
1313
|
+
const adapter = registry.adapterForFile(relPath);
|
|
1314
|
+
for (const specifier of extraction.imports) {
|
|
1315
|
+
const to = adapter.resolveImportToFile(specifier, relPath, knownFiles);
|
|
1316
|
+
if (to && to !== relPath) fileDeps.push({ from: relPath, to, kind: "import" });
|
|
1317
|
+
}
|
|
1318
|
+
for (const specifier of extraction.reExportSpecifiers) {
|
|
1319
|
+
const to = adapter.resolveImportToFile(specifier, relPath, knownFiles);
|
|
1320
|
+
if (to && to !== relPath) fileDeps.push({ from: relPath, to, kind: "re-export" });
|
|
1321
|
+
}
|
|
1322
|
+
for (const specifier of extraction.exportAllSpecifiers) {
|
|
1323
|
+
const to = adapter.resolveImportToFile(specifier, relPath, knownFiles);
|
|
1324
|
+
if (to && to !== relPath) fileDeps.push({ from: relPath, to, kind: "export-all" });
|
|
1325
|
+
}
|
|
1326
|
+
for (const sym of extraction.symbols) {
|
|
1327
|
+
symbols.push({
|
|
1328
|
+
file: relPath,
|
|
1329
|
+
name: sym.name,
|
|
1330
|
+
kind: sym.kind,
|
|
1331
|
+
exported: sym.exported,
|
|
1332
|
+
line: sym.location.line
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1337
|
+
const uniqueDeps = fileDeps.filter((e) => {
|
|
1338
|
+
const key = `${e.from}\0${e.to}\0${e.kind}`;
|
|
1339
|
+
if (seen.has(key)) return false;
|
|
1340
|
+
seen.add(key);
|
|
1341
|
+
return true;
|
|
1342
|
+
});
|
|
1343
|
+
uniqueDeps.sort(
|
|
1344
|
+
(a, b) => a.from < b.from ? -1 : a.from > b.from ? 1 : a.to < b.to ? -1 : a.to > b.to ? 1 : 0
|
|
1345
|
+
);
|
|
1346
|
+
symbols.sort(
|
|
1347
|
+
(a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
1348
|
+
);
|
|
1349
|
+
return { fileDeps: uniqueDeps, symbols };
|
|
1350
|
+
}
|
|
1351
|
+
function emitBuildProgress(config, phase, message, startTime, filesIndexed, totalFiles) {
|
|
1352
|
+
config.onProgress?.({
|
|
1353
|
+
phase,
|
|
1354
|
+
message,
|
|
1355
|
+
elapsedMs: Date.now() - startTime,
|
|
1356
|
+
filesIndexed,
|
|
1357
|
+
totalFiles
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/indexing/buildIndexManifest.ts
|
|
1362
|
+
function buildIndexManifest(options) {
|
|
1363
|
+
return {
|
|
1364
|
+
artifactKind: "my-dev-kit-v1-manifest",
|
|
1365
|
+
version: "1.0.0",
|
|
1366
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1367
|
+
projectRoot: options.projectRoot,
|
|
1368
|
+
sourceRoots: options.sourceRoots,
|
|
1369
|
+
languages: options.languages,
|
|
1370
|
+
callGraphEnabled: options.callGraphEnabled,
|
|
1371
|
+
artifacts: {
|
|
1372
|
+
symbolIndex: "symbol-index.json",
|
|
1373
|
+
codeGraph: "code-graph.json",
|
|
1374
|
+
callGraph: options.callGraphProduced ? "call-graph.json" : null
|
|
1375
|
+
},
|
|
1376
|
+
summary: {
|
|
1377
|
+
fileCount: options.symbolIndex.fileCount,
|
|
1378
|
+
symbolCount: options.symbolIndex.symbolCount,
|
|
1379
|
+
edgeCount: options.codeGraph.summary.edgeCount,
|
|
1380
|
+
warningCount: options.warnings.length,
|
|
1381
|
+
errorCount: options.errors.length
|
|
1382
|
+
},
|
|
1383
|
+
warnings: options.warnings,
|
|
1384
|
+
errors: options.errors
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/indexing/writeIndexManifest.ts
|
|
1389
|
+
import * as fs3 from "fs";
|
|
1390
|
+
import * as path8 from "path";
|
|
1391
|
+
function writeIndexArtifacts(options) {
|
|
1392
|
+
fs3.mkdirSync(options.outputDir, { recursive: true });
|
|
1393
|
+
writeJson(path8.join(options.outputDir, "manifest.json"), options.manifest);
|
|
1394
|
+
writeJson(path8.join(options.outputDir, "symbol-index.json"), options.symbolIndex);
|
|
1395
|
+
writeJson(path8.join(options.outputDir, "code-graph.json"), options.codeGraph);
|
|
1396
|
+
if (options.callGraph) {
|
|
1397
|
+
writeJson(path8.join(options.outputDir, "call-graph.json"), options.callGraph);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function writeJson(filePath, value) {
|
|
1401
|
+
fs3.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
|
|
1402
|
+
`, "utf8");
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/indexing/runIndexCommand.ts
|
|
1406
|
+
var SUPPORTED_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "javascript", "python"]);
|
|
1407
|
+
async function runIndexCommand(options) {
|
|
1408
|
+
const commandStartTime = Date.now();
|
|
1409
|
+
const projectRoot = path9.resolve(options.root ?? ".");
|
|
1410
|
+
const sourceRoots = options.src ?? [];
|
|
1411
|
+
const warnings = [];
|
|
1412
|
+
const errors = [];
|
|
1413
|
+
if (sourceRoots.length === 0) {
|
|
1414
|
+
throw new Error("The index command requires at least one --src <path> source root.");
|
|
1415
|
+
}
|
|
1416
|
+
if (options.language && !SUPPORTED_LANGUAGES.has(options.language)) {
|
|
1417
|
+
throw new Error(`Unsupported language "${options.language}". Supported values: typescript, javascript, python.`);
|
|
1418
|
+
}
|
|
1419
|
+
const normalizedSourceRoots = sourceRoots.map((sourceRoot) => toForwardSlash(sourceRoot));
|
|
1420
|
+
for (const sourceRoot of normalizedSourceRoots) {
|
|
1421
|
+
const absoluteSourceRoot = path9.resolve(projectRoot, sourceRoot);
|
|
1422
|
+
if (!fs4.existsSync(absoluteSourceRoot) || !fs4.statSync(absoluteSourceRoot).isDirectory()) {
|
|
1423
|
+
throw new Error(`Source root does not exist or is not a directory: ${sourceRoot}`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const outputDir = path9.resolve(projectRoot, options.out ?? ".my-dev-kit-v1");
|
|
1427
|
+
const progress = createProgressReporter(options.progress === true, commandStartTime);
|
|
1428
|
+
if (options.dryRun) {
|
|
1429
|
+
const discovery = discoverSourceFiles({
|
|
1430
|
+
repoRoot: projectRoot,
|
|
1431
|
+
sourceRoots: normalizedSourceRoots,
|
|
1432
|
+
userExcludes: options.exclude,
|
|
1433
|
+
onProgress: progress
|
|
1434
|
+
});
|
|
1435
|
+
return buildDryRunResult(projectRoot, normalizedSourceRoots, outputDir, discovery);
|
|
1436
|
+
}
|
|
1437
|
+
const buildResult = buildIndex({
|
|
1438
|
+
repoRoot: projectRoot,
|
|
1439
|
+
sourceRoots: normalizedSourceRoots,
|
|
1440
|
+
buildCallGraph: options.callGraph === true,
|
|
1441
|
+
excludePatterns: options.exclude,
|
|
1442
|
+
onProgress: progress
|
|
1443
|
+
});
|
|
1444
|
+
const languages = inferLanguages(buildResult.index.files.map((file) => file.language), options.language);
|
|
1445
|
+
const codeGraph = buildCodeGraph({
|
|
1446
|
+
symbolIndex: buildResult.index,
|
|
1447
|
+
callGraph: buildResult.callGraph
|
|
1448
|
+
});
|
|
1449
|
+
const manifest = buildIndexManifest({
|
|
1450
|
+
projectRoot: toForwardSlash(projectRoot),
|
|
1451
|
+
sourceRoots: normalizedSourceRoots,
|
|
1452
|
+
languages,
|
|
1453
|
+
callGraphEnabled: options.callGraph === true,
|
|
1454
|
+
callGraphProduced: buildResult.callGraph !== null,
|
|
1455
|
+
symbolIndex: buildResult.index,
|
|
1456
|
+
codeGraph,
|
|
1457
|
+
warnings,
|
|
1458
|
+
errors
|
|
1459
|
+
});
|
|
1460
|
+
progress?.({
|
|
1461
|
+
phase: "artifact-write-start",
|
|
1462
|
+
message: "Final artifact writing started",
|
|
1463
|
+
elapsedMs: Date.now() - commandStartTime
|
|
1464
|
+
});
|
|
1465
|
+
writeIndexArtifacts({
|
|
1466
|
+
outputDir,
|
|
1467
|
+
manifest,
|
|
1468
|
+
symbolIndex: buildResult.index,
|
|
1469
|
+
codeGraph,
|
|
1470
|
+
callGraph: buildResult.callGraph
|
|
1471
|
+
});
|
|
1472
|
+
progress?.({
|
|
1473
|
+
phase: "artifact-write-complete",
|
|
1474
|
+
message: "Final artifact writing completed",
|
|
1475
|
+
elapsedMs: Date.now() - commandStartTime
|
|
1476
|
+
});
|
|
1477
|
+
return {
|
|
1478
|
+
mode: "index",
|
|
1479
|
+
manifest,
|
|
1480
|
+
outputDir: toForwardSlash(outputDir),
|
|
1481
|
+
symbolIndexPath: toForwardSlash(path9.join(outputDir, "symbol-index.json")),
|
|
1482
|
+
codeGraphPath: toForwardSlash(path9.join(outputDir, "code-graph.json")),
|
|
1483
|
+
callGraphPath: buildResult.callGraph ? toForwardSlash(path9.join(outputDir, "call-graph.json")) : null
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
function inferLanguages(languages, requestedLanguage) {
|
|
1487
|
+
if (requestedLanguage) return [requestedLanguage];
|
|
1488
|
+
return [...new Set(languages)].sort();
|
|
1489
|
+
}
|
|
1490
|
+
function buildDryRunResult(projectRoot, sourceRoots, outputDir, discovery) {
|
|
1491
|
+
return {
|
|
1492
|
+
mode: "dry-run",
|
|
1493
|
+
projectRoot: toForwardSlash(projectRoot),
|
|
1494
|
+
sourceRoots,
|
|
1495
|
+
outputDir: toForwardSlash(outputDir),
|
|
1496
|
+
defaultIgnoredDirectoryNames: discovery.defaultIgnoredDirectoryNames,
|
|
1497
|
+
userExcludes: discovery.userExcludes,
|
|
1498
|
+
totalFilesDiscovered: discovery.totalFilesDiscovered,
|
|
1499
|
+
totalFilesEligibleForIndexing: discovery.totalFilesEligibleForIndexing,
|
|
1500
|
+
totalFilesSkipped: discovery.totalFilesSkipped,
|
|
1501
|
+
skippedByDefaultIgnore: discovery.skippedByDefaultIgnore,
|
|
1502
|
+
skippedByUserExclude: discovery.skippedByUserExclude,
|
|
1503
|
+
skippedByFilePattern: discovery.skippedByFilePattern,
|
|
1504
|
+
skippedUnsupportedFiles: discovery.skippedUnsupportedFiles,
|
|
1505
|
+
languageCounts: discovery.languageCounts,
|
|
1506
|
+
largestFiles: discovery.largestFiles,
|
|
1507
|
+
sampleIndexedFiles: discovery.sampleIndexedFiles,
|
|
1508
|
+
sampleSkippedFiles: discovery.sampleSkippedFiles
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function createProgressReporter(enabled, commandStartTime) {
|
|
1512
|
+
if (!enabled) return void 0;
|
|
1513
|
+
return (event) => {
|
|
1514
|
+
const elapsedSeconds = ((event.elapsedMs || Date.now() - commandStartTime) / 1e3).toFixed(1);
|
|
1515
|
+
const counts = "filesEligible" in event ? ` discovered=${event.filesDiscovered} eligible=${event.filesEligible} skipped=${event.filesSkipped}` : "filesIndexed" in event ? ` indexed=${event.filesIndexed}/${event.totalFiles}` : "";
|
|
1516
|
+
const sourceRoot = "currentSourceRoot" in event && event.currentSourceRoot ? ` source=${event.currentSourceRoot}` : "";
|
|
1517
|
+
process.stderr.write(`[my-dev-kit:index] ${event.message} (${elapsedSeconds}s)${sourceRoot}${counts}
|
|
1518
|
+
`);
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/commands/indexCommand.ts
|
|
1523
|
+
function registerIndexCommand(program2) {
|
|
1524
|
+
program2.command("index").description("Index a local TypeScript, JavaScript, or Python codebase.").option("--root <path>", "project root", ".").option("--src <path>", "source root to index; may be repeated", collectValues, []).option("--language <language>", "source language: typescript, javascript, or python").option("--out <dir>", "output directory", ".my-dev-kit-v1").option("--exclude <path-or-name>", "directory name or relative path prefix to exclude; may be repeated", collectValues, []).option("--dry-run", "scan and report what would be indexed without writing artifacts").option("--progress", "print bounded progress diagnostics to stderr").option("--call-graph", "include call graph when supported").option("--json", "print JSON output").action(async (options) => {
|
|
1525
|
+
const result = await runIndexCommand(options);
|
|
1526
|
+
if (options.json) {
|
|
1527
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
if (result.mode === "dry-run") {
|
|
1531
|
+
printDryRunSummary(result);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
console.log(`Indexed ${result.manifest.summary.fileCount} file(s) and ${result.manifest.summary.symbolCount} symbol(s).`);
|
|
1535
|
+
console.log(`Output: ${result.outputDir}`);
|
|
1536
|
+
console.log("Artifacts: manifest.json, symbol-index.json, code-graph.json");
|
|
1537
|
+
if (result.callGraphPath) {
|
|
1538
|
+
console.log("Call graph: call-graph.json");
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
function collectValues(value, previous) {
|
|
1543
|
+
return [...previous, value];
|
|
1544
|
+
}
|
|
1545
|
+
function printDryRunSummary(result) {
|
|
1546
|
+
console.log("Dry-run scan completed.");
|
|
1547
|
+
console.log(`Project root: ${result.projectRoot}`);
|
|
1548
|
+
console.log(`Source roots: ${result.sourceRoots.join(", ")}`);
|
|
1549
|
+
console.log(`Output directory: ${result.outputDir}`);
|
|
1550
|
+
console.log(`Eligible files: ${result.totalFilesEligibleForIndexing}`);
|
|
1551
|
+
console.log(`Files discovered: ${result.totalFilesDiscovered}`);
|
|
1552
|
+
console.log(`Skipped paths: ${result.totalFilesSkipped}`);
|
|
1553
|
+
console.log(`Skipped by default ignore: ${result.skippedByDefaultIgnore}`);
|
|
1554
|
+
console.log(`Skipped by user exclude: ${result.skippedByUserExclude}`);
|
|
1555
|
+
console.log(`Language counts: ${JSON.stringify(result.languageCounts)}`);
|
|
1556
|
+
if (result.largestFiles.length > 0) {
|
|
1557
|
+
console.log("Largest files:");
|
|
1558
|
+
for (const file of result.largestFiles) {
|
|
1559
|
+
console.log(`- ${file.path} (${file.sizeBytes} bytes)`);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if (result.sampleIndexedFiles.length > 0) {
|
|
1563
|
+
console.log("Sample indexed files:");
|
|
1564
|
+
for (const file of result.sampleIndexedFiles) console.log(`- ${file}`);
|
|
1565
|
+
}
|
|
1566
|
+
if (result.sampleSkippedFiles.length > 0) {
|
|
1567
|
+
console.log("Sample skipped paths:");
|
|
1568
|
+
for (const file of result.sampleSkippedFiles) console.log(`- ${file.path} (${file.reason})`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/indexing/loadIndexArtifacts.ts
|
|
1573
|
+
import * as fs6 from "fs";
|
|
1574
|
+
|
|
1575
|
+
// src/indexing/readIndexManifest.ts
|
|
1576
|
+
import * as fs5 from "fs";
|
|
1577
|
+
import * as path10 from "path";
|
|
1578
|
+
function readIndexManifest(indexDirInput) {
|
|
1579
|
+
const indexDir = path10.resolve(indexDirInput);
|
|
1580
|
+
const manifestPath = path10.join(indexDir, "manifest.json");
|
|
1581
|
+
if (!fs5.existsSync(manifestPath)) {
|
|
1582
|
+
throw new Error(`Missing index manifest: ${manifestPath}`);
|
|
1583
|
+
}
|
|
1584
|
+
const manifest = readJson(manifestPath);
|
|
1585
|
+
validateManifest(manifest);
|
|
1586
|
+
return {
|
|
1587
|
+
indexDir,
|
|
1588
|
+
manifestPath,
|
|
1589
|
+
manifest,
|
|
1590
|
+
artifactPaths: {
|
|
1591
|
+
symbolIndex: resolveArtifactPath(indexDir, manifest.artifacts.symbolIndex, "symbolIndex"),
|
|
1592
|
+
codeGraph: resolveArtifactPath(indexDir, manifest.artifacts.codeGraph, "codeGraph"),
|
|
1593
|
+
callGraph: manifest.artifacts.callGraph ? resolveArtifactPath(indexDir, manifest.artifacts.callGraph, "callGraph") : null
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function readJson(filePath) {
|
|
1598
|
+
try {
|
|
1599
|
+
return JSON.parse(fs5.readFileSync(filePath, "utf8"));
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
if (error instanceof SyntaxError) {
|
|
1602
|
+
throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
|
|
1603
|
+
}
|
|
1604
|
+
throw new Error(`Failed to read ${filePath}: ${error.message}`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function validateManifest(value) {
|
|
1608
|
+
if (!value || typeof value !== "object") throw new Error("Invalid manifest shape: expected an object.");
|
|
1609
|
+
if (value.artifactKind !== "my-dev-kit-v1-manifest") {
|
|
1610
|
+
throw new Error("Invalid manifest shape: artifactKind must be my-dev-kit-v1-manifest.");
|
|
1611
|
+
}
|
|
1612
|
+
if (!value.version) throw new Error("Invalid manifest shape: version is required.");
|
|
1613
|
+
if (!value.projectRoot) throw new Error("Invalid manifest shape: projectRoot is required.");
|
|
1614
|
+
if (!Array.isArray(value.sourceRoots)) throw new Error("Invalid manifest shape: sourceRoots must be an array.");
|
|
1615
|
+
if (!value.artifacts || typeof value.artifacts !== "object") {
|
|
1616
|
+
throw new Error("Invalid manifest shape: artifacts is required.");
|
|
1617
|
+
}
|
|
1618
|
+
if (!value.artifacts.symbolIndex) throw new Error("Invalid manifest shape: artifacts.symbolIndex is required.");
|
|
1619
|
+
if (!value.artifacts.codeGraph) throw new Error("Invalid manifest shape: artifacts.codeGraph is required.");
|
|
1620
|
+
}
|
|
1621
|
+
function resolveArtifactPath(indexDir, artifactPath, name) {
|
|
1622
|
+
if (!artifactPath) throw new Error(`Missing required artifact path: ${name}`);
|
|
1623
|
+
const resolved = path10.resolve(indexDir, artifactPath);
|
|
1624
|
+
if (!isInsideRoot(indexDir, resolved)) {
|
|
1625
|
+
throw new Error(`Artifact path for "${name}" escapes the index directory: ${artifactPath}`);
|
|
1626
|
+
}
|
|
1627
|
+
return resolved;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/indexing/loadIndexArtifacts.ts
|
|
1631
|
+
function loadLookupArtifacts(indexDir) {
|
|
1632
|
+
const resolved = readIndexManifest(indexDir);
|
|
1633
|
+
return {
|
|
1634
|
+
resolved,
|
|
1635
|
+
codeGraph: readRequiredJson(resolved.artifactPaths.codeGraph, "code graph")
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function loadSourceArtifacts(options) {
|
|
1639
|
+
const resolved = readIndexManifest(options.indexDir);
|
|
1640
|
+
return {
|
|
1641
|
+
resolved,
|
|
1642
|
+
codeGraph: options.loadCodeGraph ? readRequiredJson(resolved.artifactPaths.codeGraph, "code graph") : void 0,
|
|
1643
|
+
symbolIndex: options.loadSymbolIndex ? readRequiredJson(resolved.artifactPaths.symbolIndex, "symbol index") : void 0
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
function readRequiredJson(filePath, label) {
|
|
1647
|
+
if (!fs6.existsSync(filePath)) throw new Error(`Missing required ${label} artifact: ${filePath}`);
|
|
1648
|
+
try {
|
|
1649
|
+
return JSON.parse(fs6.readFileSync(filePath, "utf8"));
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
if (error instanceof SyntaxError) throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
|
|
1652
|
+
throw new Error(`Failed to read ${filePath}: ${error.message}`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/lookup/lookupNode.ts
|
|
1657
|
+
function lookupNode(options) {
|
|
1658
|
+
validateDepth(options.depth);
|
|
1659
|
+
const node = options.graph.nodes.find((candidate) => candidate.id === options.nodeId);
|
|
1660
|
+
if (!node) throw new Error(`Node not found: ${options.nodeId}`);
|
|
1661
|
+
const incomingEdges = options.graph.edges.filter((edge) => edge.target === options.nodeId).sort(compareById);
|
|
1662
|
+
const outgoingEdges = options.graph.edges.filter((edge) => edge.source === options.nodeId).sort(compareById);
|
|
1663
|
+
const neighbors = collectNeighbors(options.graph, options.nodeId, options.depth);
|
|
1664
|
+
return {
|
|
1665
|
+
status: "found",
|
|
1666
|
+
indexDir: options.indexDir,
|
|
1667
|
+
nodeId: options.nodeId,
|
|
1668
|
+
depth: options.depth,
|
|
1669
|
+
node,
|
|
1670
|
+
incomingEdges,
|
|
1671
|
+
outgoingEdges,
|
|
1672
|
+
neighbors,
|
|
1673
|
+
artifactPaths: {
|
|
1674
|
+
manifest: options.manifestPath,
|
|
1675
|
+
codeGraph: options.codeGraphPath
|
|
1676
|
+
},
|
|
1677
|
+
warnings: []
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function validateDepth(depth) {
|
|
1681
|
+
if (!Number.isInteger(depth)) throw new Error("Depth must be an integer.");
|
|
1682
|
+
if (depth < 0) throw new Error("Depth must be 0 or greater.");
|
|
1683
|
+
if (depth > 3) throw new Error("Depth greater than 3 is not supported.");
|
|
1684
|
+
}
|
|
1685
|
+
function collectNeighbors(graph, nodeId, depth) {
|
|
1686
|
+
if (depth === 0) return [];
|
|
1687
|
+
const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1688
|
+
const visited = /* @__PURE__ */ new Set([nodeId]);
|
|
1689
|
+
const neighborIds = /* @__PURE__ */ new Set();
|
|
1690
|
+
let frontier = /* @__PURE__ */ new Set([nodeId]);
|
|
1691
|
+
for (let level = 0; level < depth; level++) {
|
|
1692
|
+
const next = /* @__PURE__ */ new Set();
|
|
1693
|
+
for (const current of frontier) {
|
|
1694
|
+
for (const edge of graph.edges) {
|
|
1695
|
+
const adjacent = edge.source === current ? edge.target : edge.target === current ? edge.source : null;
|
|
1696
|
+
if (!adjacent || visited.has(adjacent)) continue;
|
|
1697
|
+
visited.add(adjacent);
|
|
1698
|
+
neighborIds.add(adjacent);
|
|
1699
|
+
next.add(adjacent);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
frontier = next;
|
|
1703
|
+
}
|
|
1704
|
+
return [...neighborIds].map((id) => nodesById.get(id)).filter((node) => node !== void 0).sort(compareById);
|
|
1705
|
+
}
|
|
1706
|
+
function compareById(a, b) {
|
|
1707
|
+
return a.id.localeCompare(b.id);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/commands/parseUtils.ts
|
|
1711
|
+
function parseInteger(value) {
|
|
1712
|
+
const parsed = Number(value);
|
|
1713
|
+
if (!Number.isInteger(parsed)) throw new Error(`Expected an integer, got "${value}".`);
|
|
1714
|
+
return parsed;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// src/commands/lookupCommand.ts
|
|
1718
|
+
function registerLookupCommand(program2) {
|
|
1719
|
+
program2.command("lookup").description("Look up an indexed graph node.").option("--index <dir>", "index artifact directory", ".my-dev-kit-v1").option("--node <node-id>", "node id to look up").option("--depth <n>", "traversal depth", parseInteger, 1).option("--json", "print JSON output").action((options) => {
|
|
1720
|
+
if (!options.node) throw new Error("The lookup command requires --node <node-id>.");
|
|
1721
|
+
const artifacts = loadLookupArtifacts(options.index);
|
|
1722
|
+
const result = lookupNode({
|
|
1723
|
+
graph: artifacts.codeGraph,
|
|
1724
|
+
indexDir: options.index,
|
|
1725
|
+
nodeId: options.node,
|
|
1726
|
+
depth: options.depth,
|
|
1727
|
+
manifestPath: `${options.index}/manifest.json`,
|
|
1728
|
+
codeGraphPath: `${options.index}/${artifacts.resolved.manifest.artifacts.codeGraph}`
|
|
1729
|
+
});
|
|
1730
|
+
if (options.json) {
|
|
1731
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
console.log(`Found ${result.node.kind} node: ${result.node.id}`);
|
|
1735
|
+
console.log(`Incoming edges: ${result.incomingEdges.length}`);
|
|
1736
|
+
console.log(`Outgoing edges: ${result.outgoingEdges.length}`);
|
|
1737
|
+
console.log(`Neighbors within depth ${result.depth}: ${result.neighbors.length}`);
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// src/commands/searchCommand.ts
|
|
1742
|
+
import * as fs7 from "fs";
|
|
1743
|
+
|
|
1744
|
+
// src/search/rankSearchResults.ts
|
|
1745
|
+
var ALL_TERMS_BONUS = 10;
|
|
1746
|
+
var EXACT_PHRASE_MULTIPLIER = 2;
|
|
1747
|
+
var MAX_MATCH_TEXT_LENGTH = 120;
|
|
1748
|
+
function normalizeSearchQuery(query) {
|
|
1749
|
+
return query.trim().toLowerCase().split(/[\s.,;:()[\]{}<>"'`|\\/!?@#$%^&*+=~-]+/u).map((term) => term.trim()).filter(Boolean);
|
|
1750
|
+
}
|
|
1751
|
+
function rankSearchResults(options) {
|
|
1752
|
+
const phrase = options.query.trim().toLowerCase();
|
|
1753
|
+
const ranked = options.candidates.map((candidate) => scoreCandidate(candidate, phrase, options.normalizedTerms)).filter((item) => item !== null).sort(compareResults);
|
|
1754
|
+
return ranked.slice(0, options.limit);
|
|
1755
|
+
}
|
|
1756
|
+
function scoreCandidate(candidate, phrase, terms) {
|
|
1757
|
+
const reasons = [];
|
|
1758
|
+
const matchedTerms = /* @__PURE__ */ new Set();
|
|
1759
|
+
for (const field2 of candidate.fields) {
|
|
1760
|
+
const normalizedText = field2.text.toLowerCase();
|
|
1761
|
+
if (phrase && normalizedText.includes(phrase)) {
|
|
1762
|
+
reasons.push({
|
|
1763
|
+
field: field2.field,
|
|
1764
|
+
term: phrase,
|
|
1765
|
+
weight: field2.weight * EXACT_PHRASE_MULTIPLIER,
|
|
1766
|
+
text: shorten(field2.text)
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
for (const term of terms) {
|
|
1770
|
+
if (!normalizedText.includes(term)) continue;
|
|
1771
|
+
matchedTerms.add(term);
|
|
1772
|
+
reasons.push({
|
|
1773
|
+
field: field2.field,
|
|
1774
|
+
term,
|
|
1775
|
+
weight: field2.weight,
|
|
1776
|
+
text: shorten(field2.text)
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (reasons.length === 0) return null;
|
|
1781
|
+
const dedupedReasons = dedupeReasons(reasons);
|
|
1782
|
+
const baseScore = dedupedReasons.reduce((sum, reason) => sum + reason.weight, 0);
|
|
1783
|
+
const score = baseScore + (terms.length > 0 && matchedTerms.size === terms.length ? ALL_TERMS_BONUS : 0);
|
|
1784
|
+
return {
|
|
1785
|
+
...candidate.item,
|
|
1786
|
+
score,
|
|
1787
|
+
matchReasons: dedupedReasons.sort(compareReasons)
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
function dedupeReasons(reasons) {
|
|
1791
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1792
|
+
const result = [];
|
|
1793
|
+
for (const reason of reasons) {
|
|
1794
|
+
const key = `${reason.field}\0${reason.term}\0${reason.text}\0${reason.weight}`;
|
|
1795
|
+
if (seen.has(key)) continue;
|
|
1796
|
+
seen.add(key);
|
|
1797
|
+
result.push(reason);
|
|
1798
|
+
}
|
|
1799
|
+
return result;
|
|
1800
|
+
}
|
|
1801
|
+
function compareResults(a, b) {
|
|
1802
|
+
return b.score - a.score || a.kind.localeCompare(b.kind) || stableResultKey(a).localeCompare(stableResultKey(b));
|
|
1803
|
+
}
|
|
1804
|
+
function stableResultKey(item) {
|
|
1805
|
+
return item.path ?? item.id;
|
|
1806
|
+
}
|
|
1807
|
+
function compareReasons(a, b) {
|
|
1808
|
+
return b.weight - a.weight || a.field.localeCompare(b.field) || a.term.localeCompare(b.term) || a.text.localeCompare(b.text);
|
|
1809
|
+
}
|
|
1810
|
+
function shorten(text) {
|
|
1811
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
1812
|
+
if (compact.length <= MAX_MATCH_TEXT_LENGTH) return compact;
|
|
1813
|
+
return `${compact.slice(0, MAX_MATCH_TEXT_LENGTH - 3)}...`;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// src/search/searchIndex.ts
|
|
1817
|
+
var DEFAULT_LIMIT = 20;
|
|
1818
|
+
var WEIGHTS = {
|
|
1819
|
+
path: 8,
|
|
1820
|
+
label: 6,
|
|
1821
|
+
symbolName: 12,
|
|
1822
|
+
symbolKind: 4,
|
|
1823
|
+
import: 5,
|
|
1824
|
+
export: 10,
|
|
1825
|
+
edgeKind: 3,
|
|
1826
|
+
nodeId: 4,
|
|
1827
|
+
neighbor: 2
|
|
1828
|
+
};
|
|
1829
|
+
var EDGE_WEIGHTS = {
|
|
1830
|
+
nodeId: 1,
|
|
1831
|
+
neighbor: 1
|
|
1832
|
+
};
|
|
1833
|
+
function searchIndex(input) {
|
|
1834
|
+
validateArtifacts(input.symbolIndex, input.codeGraph);
|
|
1835
|
+
const normalizedTerms = normalizeSearchQuery(input.query);
|
|
1836
|
+
if (normalizedTerms.length === 0) throw new Error("Search query must include at least one non-empty term.");
|
|
1837
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
1838
|
+
const candidates = buildCandidates(input.symbolIndex, input.codeGraph);
|
|
1839
|
+
const results = rankSearchResults({
|
|
1840
|
+
candidates,
|
|
1841
|
+
query: input.query,
|
|
1842
|
+
normalizedTerms,
|
|
1843
|
+
limit
|
|
1844
|
+
});
|
|
1845
|
+
const searchedFileIds = /* @__PURE__ */ new Set();
|
|
1846
|
+
const searchedSymbolIds = /* @__PURE__ */ new Set();
|
|
1847
|
+
for (const candidate of candidates) {
|
|
1848
|
+
if (candidate.item.kind === "file") searchedFileIds.add(candidate.item.id);
|
|
1849
|
+
if (candidate.item.kind === "symbol") searchedSymbolIds.add(candidate.item.id);
|
|
1850
|
+
}
|
|
1851
|
+
return {
|
|
1852
|
+
artifactKind: "my-dev-kit-v1-search-result",
|
|
1853
|
+
version: "1.0.0",
|
|
1854
|
+
createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1855
|
+
indexDir: input.resolved.indexDir,
|
|
1856
|
+
query: input.query,
|
|
1857
|
+
normalizedTerms,
|
|
1858
|
+
limit,
|
|
1859
|
+
results,
|
|
1860
|
+
summary: {
|
|
1861
|
+
resultCount: results.length,
|
|
1862
|
+
searchedFileCount: searchedFileIds.size,
|
|
1863
|
+
searchedSymbolCount: searchedSymbolIds.size,
|
|
1864
|
+
searchedEdgeCount: input.codeGraph.edges.length
|
|
1865
|
+
},
|
|
1866
|
+
artifactPaths: {
|
|
1867
|
+
manifest: input.resolved.manifestPath,
|
|
1868
|
+
symbolIndex: input.resolved.artifactPaths.symbolIndex,
|
|
1869
|
+
codeGraph: input.resolved.artifactPaths.codeGraph
|
|
1870
|
+
},
|
|
1871
|
+
warnings: []
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
function buildCandidates(symbolIndex, codeGraph) {
|
|
1875
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
1876
|
+
const nodesById = new Map(codeGraph.nodes.map((node) => [node.id, node]));
|
|
1877
|
+
for (const node of codeGraph.nodes) {
|
|
1878
|
+
mergeCandidate(candidates, nodeCandidate(node));
|
|
1879
|
+
}
|
|
1880
|
+
for (const file of symbolIndex.files) {
|
|
1881
|
+
mergeCandidate(candidates, fileCandidate(file));
|
|
1882
|
+
for (const symbol of file.symbols) {
|
|
1883
|
+
mergeCandidate(candidates, symbolCandidate(file, symbol));
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
for (const dep of symbolIndex.graph?.fileDeps ?? []) {
|
|
1887
|
+
mergeCandidate(candidates, {
|
|
1888
|
+
item: {
|
|
1889
|
+
kind: "file",
|
|
1890
|
+
id: fileNodeId2(dep.from),
|
|
1891
|
+
label: dep.from,
|
|
1892
|
+
path: dep.from,
|
|
1893
|
+
nodeId: fileNodeId2(dep.from)
|
|
1894
|
+
},
|
|
1895
|
+
fields: [
|
|
1896
|
+
field("neighbor", dep.to, WEIGHTS.neighbor),
|
|
1897
|
+
field("edgeKind", dep.kind, WEIGHTS.edgeKind)
|
|
1898
|
+
]
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
for (const edge of codeGraph.edges) {
|
|
1902
|
+
mergeCandidate(candidates, edgeCandidate(edge, nodesById));
|
|
1903
|
+
}
|
|
1904
|
+
return [...candidates.values()].sort((a, b) => a.item.kind.localeCompare(b.item.kind) || a.item.id.localeCompare(b.item.id));
|
|
1905
|
+
}
|
|
1906
|
+
function nodeCandidate(node) {
|
|
1907
|
+
const fields = [
|
|
1908
|
+
field("nodeId", node.id, WEIGHTS.nodeId),
|
|
1909
|
+
field("label", node.label, WEIGHTS.label)
|
|
1910
|
+
];
|
|
1911
|
+
if (node.path) fields.push(field("path", node.path, WEIGHTS.path));
|
|
1912
|
+
if (node.symbolName) fields.push(field("symbolName", node.symbolName, WEIGHTS.symbolName));
|
|
1913
|
+
if (node.symbolKind) fields.push(field("symbolKind", node.symbolKind, WEIGHTS.symbolKind));
|
|
1914
|
+
if (node.exported && node.symbolName) fields.push(field("export", node.symbolName, WEIGHTS.export));
|
|
1915
|
+
return {
|
|
1916
|
+
item: {
|
|
1917
|
+
kind: node.kind,
|
|
1918
|
+
id: node.id,
|
|
1919
|
+
label: node.label,
|
|
1920
|
+
path: node.path,
|
|
1921
|
+
nodeId: node.id
|
|
1922
|
+
},
|
|
1923
|
+
fields
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
function fileCandidate(file) {
|
|
1927
|
+
return {
|
|
1928
|
+
item: {
|
|
1929
|
+
kind: "file",
|
|
1930
|
+
id: fileNodeId2(file.path),
|
|
1931
|
+
label: file.path,
|
|
1932
|
+
path: file.path,
|
|
1933
|
+
nodeId: fileNodeId2(file.path)
|
|
1934
|
+
},
|
|
1935
|
+
fields: [
|
|
1936
|
+
field("path", file.path, WEIGHTS.path),
|
|
1937
|
+
...file.imports.map((value) => field("import", value, WEIGHTS.import)),
|
|
1938
|
+
...file.exports.map((value) => field("export", value, WEIGHTS.export))
|
|
1939
|
+
]
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
function symbolCandidate(file, symbol) {
|
|
1943
|
+
const fields = [
|
|
1944
|
+
field("path", file.path, WEIGHTS.path),
|
|
1945
|
+
field("symbolName", symbol.name, WEIGHTS.symbolName),
|
|
1946
|
+
field("symbolKind", symbol.kind, WEIGHTS.symbolKind)
|
|
1947
|
+
];
|
|
1948
|
+
if (symbol.exported) fields.push(field("export", symbol.name, WEIGHTS.export));
|
|
1949
|
+
if (symbol.signature) fields.push(field("label", symbol.signature, WEIGHTS.label));
|
|
1950
|
+
return {
|
|
1951
|
+
item: {
|
|
1952
|
+
kind: "symbol",
|
|
1953
|
+
id: symbolNodeId2(file.path, symbol.name),
|
|
1954
|
+
label: symbol.name,
|
|
1955
|
+
path: file.path,
|
|
1956
|
+
nodeId: symbolNodeId2(file.path, symbol.name)
|
|
1957
|
+
},
|
|
1958
|
+
fields
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function edgeCandidate(edge, nodesById) {
|
|
1962
|
+
const source = nodesById.get(edge.source);
|
|
1963
|
+
const target = nodesById.get(edge.target);
|
|
1964
|
+
const label = edge.label ?? edge.kind;
|
|
1965
|
+
return {
|
|
1966
|
+
item: {
|
|
1967
|
+
kind: "edge",
|
|
1968
|
+
id: edge.id,
|
|
1969
|
+
label: `${edge.source} --${label}--> ${edge.target}`,
|
|
1970
|
+
edge: {
|
|
1971
|
+
source: edge.source,
|
|
1972
|
+
target: edge.target,
|
|
1973
|
+
kind: edge.kind
|
|
1974
|
+
}
|
|
1975
|
+
},
|
|
1976
|
+
fields: [
|
|
1977
|
+
field("edgeKind", edge.kind, WEIGHTS.edgeKind),
|
|
1978
|
+
field("edgeKind", label, WEIGHTS.edgeKind),
|
|
1979
|
+
field("nodeId", edge.id, EDGE_WEIGHTS.nodeId),
|
|
1980
|
+
field("neighbor", edge.source, EDGE_WEIGHTS.neighbor),
|
|
1981
|
+
field("neighbor", edge.target, EDGE_WEIGHTS.neighbor),
|
|
1982
|
+
...source?.path ? [field("neighbor", source.path, EDGE_WEIGHTS.neighbor)] : [],
|
|
1983
|
+
...target?.path ? [field("neighbor", target.path, EDGE_WEIGHTS.neighbor)] : [],
|
|
1984
|
+
...source?.label ? [field("neighbor", source.label, EDGE_WEIGHTS.neighbor)] : [],
|
|
1985
|
+
...target?.label ? [field("neighbor", target.label, EDGE_WEIGHTS.neighbor)] : []
|
|
1986
|
+
]
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
function mergeCandidate(candidates, candidate) {
|
|
1990
|
+
const key = `${candidate.item.kind}:${candidate.item.id}`;
|
|
1991
|
+
const existing = candidates.get(key);
|
|
1992
|
+
if (!existing) {
|
|
1993
|
+
candidates.set(key, candidate);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
existing.fields.push(...candidate.fields);
|
|
1997
|
+
existing.item.label = existing.item.label || candidate.item.label;
|
|
1998
|
+
existing.item.path = existing.item.path ?? candidate.item.path;
|
|
1999
|
+
existing.item.nodeId = existing.item.nodeId ?? candidate.item.nodeId;
|
|
2000
|
+
existing.item.edge = existing.item.edge ?? candidate.item.edge;
|
|
2001
|
+
}
|
|
2002
|
+
function field(fieldName, text, weight) {
|
|
2003
|
+
return { field: fieldName, text, weight };
|
|
2004
|
+
}
|
|
2005
|
+
function validateArtifacts(symbolIndex, codeGraph) {
|
|
2006
|
+
if (!symbolIndex || typeof symbolIndex !== "object" || !Array.isArray(symbolIndex.files)) {
|
|
2007
|
+
throw new Error("Invalid symbol index artifact: files must be an array.");
|
|
2008
|
+
}
|
|
2009
|
+
if (!codeGraph || typeof codeGraph !== "object" || !Array.isArray(codeGraph.nodes) || !Array.isArray(codeGraph.edges)) {
|
|
2010
|
+
throw new Error("Invalid code graph artifact: nodes and edges must be arrays.");
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
function fileNodeId2(filePath) {
|
|
2014
|
+
return `file:${filePath}`;
|
|
2015
|
+
}
|
|
2016
|
+
function symbolNodeId2(filePath, name) {
|
|
2017
|
+
return `symbol:${filePath}#${name}`;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// src/commands/searchCommand.ts
|
|
2021
|
+
var DEFAULT_LIMIT2 = 20;
|
|
2022
|
+
var MAX_LIMIT = 100;
|
|
2023
|
+
function registerSearchCommand(program2) {
|
|
2024
|
+
program2.command("search").description("Search indexed files, symbols, and graph edges.").option("--index <dir>", "index artifact directory", ".my-dev-kit-v1").option("--query <text>", "search query").option("--limit <n>", `result limit, 1 through ${MAX_LIMIT}`, parseLimit, DEFAULT_LIMIT2).option("--json", "print JSON output").action((options) => {
|
|
2025
|
+
if (!options.query) throw new Error("The search command requires --query <text>.");
|
|
2026
|
+
const resolved = readIndexManifest(options.index);
|
|
2027
|
+
const result = searchIndex({
|
|
2028
|
+
resolved,
|
|
2029
|
+
symbolIndex: readRequiredJson2(resolved.artifactPaths.symbolIndex, "symbol index"),
|
|
2030
|
+
codeGraph: readRequiredJson2(resolved.artifactPaths.codeGraph, "code graph"),
|
|
2031
|
+
query: options.query,
|
|
2032
|
+
limit: options.limit
|
|
2033
|
+
});
|
|
2034
|
+
if (options.json) {
|
|
2035
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
printTextResult(result);
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
function parseLimit(value) {
|
|
2042
|
+
const parsed = Number(value);
|
|
2043
|
+
if (!Number.isInteger(parsed)) throw new Error(`Expected --limit to be an integer, got "${value}".`);
|
|
2044
|
+
if (parsed < 1) throw new Error("--limit must be a positive integer.");
|
|
2045
|
+
if (parsed > MAX_LIMIT) throw new Error(`--limit must be ${MAX_LIMIT} or less.`);
|
|
2046
|
+
return parsed;
|
|
2047
|
+
}
|
|
2048
|
+
function readRequiredJson2(filePath, label) {
|
|
2049
|
+
if (!fs7.existsSync(filePath)) throw new Error(`Missing required ${label} artifact: ${filePath}`);
|
|
2050
|
+
try {
|
|
2051
|
+
return JSON.parse(fs7.readFileSync(filePath, "utf8"));
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
if (error instanceof SyntaxError) throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
|
|
2054
|
+
throw new Error(`Failed to read ${filePath}: ${error.message}`);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
function printTextResult(result) {
|
|
2058
|
+
console.log(`Search query: ${result.query}`);
|
|
2059
|
+
console.log(`Index dir: ${result.indexDir}`);
|
|
2060
|
+
console.log(`Results: ${result.results.length}`);
|
|
2061
|
+
for (const [index, item] of result.results.entries()) {
|
|
2062
|
+
const target = item.path ? `${item.id} (${item.path})` : item.id;
|
|
2063
|
+
console.log(`${index + 1}. [${item.kind}] score ${item.score}: ${item.label} - ${target}`);
|
|
2064
|
+
const reasons = item.matchReasons.slice(0, 3).map((reason) => `${reason.field}:${reason.term}`).join(", ");
|
|
2065
|
+
if (reasons) console.log(` matches: ${reasons}`);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// src/graph/sliceGraph.ts
|
|
2070
|
+
function sliceGraph(options) {
|
|
2071
|
+
validateSliceInputs(options.depth, options.direction);
|
|
2072
|
+
const nodeMap = new Map(options.graph.nodes.map((node) => [node.id, node]));
|
|
2073
|
+
if (!nodeMap.has(options.focusNodeId)) throw new Error(`Node not found: ${options.focusNodeId}`);
|
|
2074
|
+
const includedNodeIds = /* @__PURE__ */ new Set([options.focusNodeId]);
|
|
2075
|
+
let frontier = /* @__PURE__ */ new Set([options.focusNodeId]);
|
|
2076
|
+
for (let level = 0; level < options.depth; level++) {
|
|
2077
|
+
const next = /* @__PURE__ */ new Set();
|
|
2078
|
+
for (const current of frontier) {
|
|
2079
|
+
for (const edge of options.graph.edges) {
|
|
2080
|
+
for (const adjacent of adjacentNodes(edge, current, options.direction)) {
|
|
2081
|
+
if (includedNodeIds.has(adjacent)) continue;
|
|
2082
|
+
includedNodeIds.add(adjacent);
|
|
2083
|
+
next.add(adjacent);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
frontier = next;
|
|
2088
|
+
}
|
|
2089
|
+
const nodes = [...includedNodeIds].map((id) => nodeMap.get(id)).filter((node) => node !== void 0).sort(compareById2);
|
|
2090
|
+
const edges = dedupeEdges(options.graph.edges).filter((edge) => includedNodeIds.has(edge.source) && includedNodeIds.has(edge.target)).filter((edge) => edgeAllowedForDirection(edge, includedNodeIds, options.focusNodeId, options.direction)).sort(compareById2);
|
|
2091
|
+
return { nodes, edges, warnings: [] };
|
|
2092
|
+
}
|
|
2093
|
+
function validateSliceInputs(depth, direction) {
|
|
2094
|
+
if (!Number.isInteger(depth)) throw new Error("Depth must be an integer.");
|
|
2095
|
+
if (depth < 0) throw new Error("Depth must be 0 or greater.");
|
|
2096
|
+
if (depth > 3) throw new Error("Depth greater than 3 is not supported.");
|
|
2097
|
+
if (!["both", "incoming", "outgoing"].includes(direction)) {
|
|
2098
|
+
throw new Error("Direction must be one of: both, incoming, outgoing.");
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
function summarizeSlice(nodes, edges) {
|
|
2102
|
+
return {
|
|
2103
|
+
nodeCount: nodes.length,
|
|
2104
|
+
edgeCount: edges.length,
|
|
2105
|
+
nodesByKind: countBy(nodes.map((node) => node.kind)),
|
|
2106
|
+
edgesByKind: countBy(edges.map((edge) => edge.kind))
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
function adjacentNodes(edge, current, direction) {
|
|
2110
|
+
if (direction === "outgoing") return edge.source === current ? [edge.target] : [];
|
|
2111
|
+
if (direction === "incoming") return edge.target === current ? [edge.source] : [];
|
|
2112
|
+
if (edge.source === current) return [edge.target];
|
|
2113
|
+
if (edge.target === current) return [edge.source];
|
|
2114
|
+
return [];
|
|
2115
|
+
}
|
|
2116
|
+
function edgeAllowedForDirection(edge, includedNodeIds, focusNodeId, direction) {
|
|
2117
|
+
if (direction === "both") return true;
|
|
2118
|
+
if (focusNodeId && includedNodeIds.size > 0) return true;
|
|
2119
|
+
return edge.source !== edge.target;
|
|
2120
|
+
}
|
|
2121
|
+
function dedupeEdges(edges) {
|
|
2122
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2123
|
+
const result = [];
|
|
2124
|
+
for (const edge of edges) {
|
|
2125
|
+
if (seen.has(edge.id)) continue;
|
|
2126
|
+
seen.add(edge.id);
|
|
2127
|
+
result.push(edge);
|
|
2128
|
+
}
|
|
2129
|
+
return result;
|
|
2130
|
+
}
|
|
2131
|
+
function countBy(values) {
|
|
2132
|
+
const counts = {};
|
|
2133
|
+
for (const value of values.sort()) counts[value] = (counts[value] ?? 0) + 1;
|
|
2134
|
+
return counts;
|
|
2135
|
+
}
|
|
2136
|
+
function compareById2(a, b) {
|
|
2137
|
+
return a.id.localeCompare(b.id);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// src/graph/writeGraphSlice.ts
|
|
2141
|
+
import * as fs8 from "fs";
|
|
2142
|
+
import * as path11 from "path";
|
|
2143
|
+
function writeGraphSlice(outputPath, slice) {
|
|
2144
|
+
const resolved = path11.resolve(outputPath);
|
|
2145
|
+
fs8.mkdirSync(path11.dirname(resolved), { recursive: true });
|
|
2146
|
+
fs8.writeFileSync(resolved, `${JSON.stringify(slice, null, 2)}
|
|
2147
|
+
`, "utf8");
|
|
2148
|
+
return resolved.replace(/\\/g, "/");
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// src/commands/sliceCommand.ts
|
|
2152
|
+
function registerSliceCommand(program2) {
|
|
2153
|
+
program2.command("slice").description("Build a bounded graph neighborhood slice.").option("--index <dir>", "index artifact directory", ".my-dev-kit-v1").option("--node <node-id>", "node id to slice around").option("--depth <n>", "slice depth", parseInteger, 1).option("--direction <both|incoming|outgoing>", "traversal direction", "both").option("--out <path>", "output path").option("--json", "print JSON output").action((options) => {
|
|
2154
|
+
if (!options.node) throw new Error("The slice command requires --node <node-id>.");
|
|
2155
|
+
const artifacts = loadLookupArtifacts(options.index);
|
|
2156
|
+
const core = sliceGraph({
|
|
2157
|
+
graph: artifacts.codeGraph,
|
|
2158
|
+
focusNodeId: options.node,
|
|
2159
|
+
depth: options.depth,
|
|
2160
|
+
direction: options.direction
|
|
2161
|
+
});
|
|
2162
|
+
const slice = {
|
|
2163
|
+
artifactKind: "my-dev-kit-v1-graph-slice",
|
|
2164
|
+
version: "1.0.0",
|
|
2165
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2166
|
+
indexDir: options.index,
|
|
2167
|
+
focusNodeId: options.node,
|
|
2168
|
+
depth: options.depth,
|
|
2169
|
+
direction: options.direction,
|
|
2170
|
+
nodes: core.nodes,
|
|
2171
|
+
edges: core.edges,
|
|
2172
|
+
summary: summarizeSlice(core.nodes, core.edges),
|
|
2173
|
+
artifactPaths: {
|
|
2174
|
+
manifest: `${options.index}/manifest.json`,
|
|
2175
|
+
codeGraph: `${options.index}/${artifacts.resolved.manifest.artifacts.codeGraph}`
|
|
2176
|
+
},
|
|
2177
|
+
warnings: core.warnings
|
|
2178
|
+
};
|
|
2179
|
+
const writtenPath = options.out ? writeGraphSlice(options.out, slice) : null;
|
|
2180
|
+
const result = { ...slice, outputPath: writtenPath };
|
|
2181
|
+
if (options.json) {
|
|
2182
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
console.log(`Graph slice: ${slice.summary.nodeCount} node(s), ${slice.summary.edgeCount} edge(s).`);
|
|
2186
|
+
if (writtenPath) console.log(`Wrote: ${writtenPath}`);
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/lookup/getSourceSlice.ts
|
|
2191
|
+
import * as fs9 from "fs";
|
|
2192
|
+
import * as path12 from "path";
|
|
2193
|
+
function ensureInsideProjectRoot(projectRoot, filePath) {
|
|
2194
|
+
const root = path12.resolve(projectRoot);
|
|
2195
|
+
const resolved = path12.resolve(root, filePath);
|
|
2196
|
+
const relative4 = path12.relative(root, resolved);
|
|
2197
|
+
if (relative4.startsWith("..") || path12.isAbsolute(relative4)) {
|
|
2198
|
+
throw new Error(`File path escapes the indexed project root: ${filePath}`);
|
|
2199
|
+
}
|
|
2200
|
+
return resolved;
|
|
2201
|
+
}
|
|
2202
|
+
function getSourceSlice(options) {
|
|
2203
|
+
validateLineRange(options.startLine, options.endLine, options.maxLines);
|
|
2204
|
+
const absolutePath = ensureInsideProjectRoot(options.projectRoot, options.filePath);
|
|
2205
|
+
if (!fs9.existsSync(absolutePath) || !fs9.statSync(absolutePath).isFile()) {
|
|
2206
|
+
throw new Error(`Source file does not exist: ${options.filePath}`);
|
|
2207
|
+
}
|
|
2208
|
+
const lines = fs9.readFileSync(absolutePath, "utf8").split(/\r?\n/);
|
|
2209
|
+
if (options.startLine > lines.length) {
|
|
2210
|
+
throw new Error(`Start line ${options.startLine} is beyond the end of file ${options.filePath}.`);
|
|
2211
|
+
}
|
|
2212
|
+
const endLine = Math.min(options.endLine, lines.length);
|
|
2213
|
+
const content = lines.slice(options.startLine - 1, endLine).join("\n");
|
|
2214
|
+
return {
|
|
2215
|
+
status: "ok",
|
|
2216
|
+
mode: options.mode,
|
|
2217
|
+
indexDir: options.indexDir,
|
|
2218
|
+
filePath: toForwardSlash(options.filePath),
|
|
2219
|
+
absolutePath: toForwardSlash(absolutePath),
|
|
2220
|
+
symbolName: options.symbolName ?? null,
|
|
2221
|
+
startLine: options.startLine,
|
|
2222
|
+
endLine,
|
|
2223
|
+
lineCount: endLine - options.startLine + 1,
|
|
2224
|
+
content,
|
|
2225
|
+
warnings: options.warnings ?? []
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
function validateLineRange(startLine, endLine, maxLines) {
|
|
2229
|
+
if (!Number.isInteger(startLine) || !Number.isInteger(endLine)) {
|
|
2230
|
+
throw new Error("Start and end lines must be positive integers.");
|
|
2231
|
+
}
|
|
2232
|
+
if (startLine < 1 || endLine < 1) throw new Error("Start and end lines must be positive integers.");
|
|
2233
|
+
if (startLine > endLine) throw new Error("Start line must be less than or equal to end line.");
|
|
2234
|
+
if (!Number.isInteger(maxLines) || maxLines < 1) throw new Error("Max lines must be a positive integer.");
|
|
2235
|
+
const requested = endLine - startLine + 1;
|
|
2236
|
+
if (requested > maxLines) {
|
|
2237
|
+
throw new Error(`Requested source slice has ${requested} lines, which exceeds --max-lines ${maxLines}.`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// src/lookup/resolveSourceTarget.ts
|
|
2242
|
+
function resolveFileNodeTarget(graph, nodeId, maxLines) {
|
|
2243
|
+
const node = findNode(graph, nodeId);
|
|
2244
|
+
if (node.kind === "file") {
|
|
2245
|
+
if (!node.path) throw new Error(`File node does not include a source path: ${nodeId}`);
|
|
2246
|
+
return {
|
|
2247
|
+
mode: "node",
|
|
2248
|
+
filePath: node.path,
|
|
2249
|
+
startLine: 1,
|
|
2250
|
+
endLine: maxLines,
|
|
2251
|
+
warnings: ["File node retrieval returns a capped preview, not the whole file."]
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
if (node.kind === "symbol") {
|
|
2255
|
+
const filePath = node.path ?? parseSymbolNodeId(node.id).filePath;
|
|
2256
|
+
const symbolName = node.symbolName ?? parseSymbolNodeId(node.id).symbolName;
|
|
2257
|
+
if (!filePath || !symbolName) throw new Error(`Symbol node does not include retrievable source metadata: ${nodeId}`);
|
|
2258
|
+
return { mode: "symbol", filePath, symbolName, warnings: [] };
|
|
2259
|
+
}
|
|
2260
|
+
throw new Error("Node is not a source-retrievable file or symbol node.");
|
|
2261
|
+
}
|
|
2262
|
+
function resolveSymbolTarget(symbolIndex, filePath, symbolName, maxLines) {
|
|
2263
|
+
const file = symbolIndex.files.find((candidate) => candidate.path === normalize2(filePath));
|
|
2264
|
+
if (!file) throw new Error(`File is not present in the current symbol index: ${filePath}`);
|
|
2265
|
+
const symbol = file.symbols.find((candidate) => candidate.name === symbolName);
|
|
2266
|
+
if (!symbol) throw new Error(`Symbol not found in ${filePath}: ${symbolName}`);
|
|
2267
|
+
if (!symbol.location || typeof symbol.location.line !== "number") {
|
|
2268
|
+
throw new Error("Symbol was found, but source location is not available in the current symbol index.");
|
|
2269
|
+
}
|
|
2270
|
+
const endLine = Math.min(file.lineCount, symbol.location.line + Math.min(maxLines, 20) - 1);
|
|
2271
|
+
return {
|
|
2272
|
+
mode: "symbol",
|
|
2273
|
+
filePath: file.path,
|
|
2274
|
+
symbolName: symbol.name,
|
|
2275
|
+
startLine: symbol.location.line,
|
|
2276
|
+
endLine,
|
|
2277
|
+
warnings: ["Symbol location has a start line only; returning a small bounded preview from that line."]
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
function findNode(graph, nodeId) {
|
|
2281
|
+
const node = graph.nodes.find((candidate) => candidate.id === nodeId);
|
|
2282
|
+
if (!node) throw new Error(`Node not found: ${nodeId}`);
|
|
2283
|
+
return node;
|
|
2284
|
+
}
|
|
2285
|
+
function parseSymbolNodeId(nodeId) {
|
|
2286
|
+
if (!nodeId.startsWith("symbol:")) return { filePath: null, symbolName: null };
|
|
2287
|
+
const body = nodeId.slice("symbol:".length);
|
|
2288
|
+
const marker = body.lastIndexOf("#");
|
|
2289
|
+
if (marker < 0) return { filePath: null, symbolName: null };
|
|
2290
|
+
return { filePath: body.slice(0, marker), symbolName: body.slice(marker + 1) };
|
|
2291
|
+
}
|
|
2292
|
+
function normalize2(filePath) {
|
|
2293
|
+
return filePath.replace(/\\/g, "/");
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// src/source/renderSourceOutput.ts
|
|
2297
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
2298
|
+
import * as path13 from "path";
|
|
2299
|
+
function parseSourceOutputFormat(value) {
|
|
2300
|
+
if (value === "json" || value === "plain" || value === "numbered") return value;
|
|
2301
|
+
throw new Error(`Unsupported --format value "${value}". Supported values: json, plain, numbered.`);
|
|
2302
|
+
}
|
|
2303
|
+
function renderSourceOutput(result, format) {
|
|
2304
|
+
if (format === "json") return JSON.stringify(result, null, 2) + "\n";
|
|
2305
|
+
if (format === "plain") return result.content.endsWith("\n") ? result.content : result.content + "\n";
|
|
2306
|
+
return renderNumberedSource(result.content, result.startLine);
|
|
2307
|
+
}
|
|
2308
|
+
function renderNumberedSource(content, startLine) {
|
|
2309
|
+
const lines = content.split("\n");
|
|
2310
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
2311
|
+
const lastLine = startLine + lines.length - 1;
|
|
2312
|
+
const width = String(lastLine).length;
|
|
2313
|
+
const numbered = lines.map((line, i) => {
|
|
2314
|
+
const num = String(startLine + i).padStart(width);
|
|
2315
|
+
return `${num} | ${line}`;
|
|
2316
|
+
});
|
|
2317
|
+
return numbered.join("\n") + "\n";
|
|
2318
|
+
}
|
|
2319
|
+
function writeSourceOutput(outputPath, rendered) {
|
|
2320
|
+
const resolved = path13.resolve(outputPath);
|
|
2321
|
+
mkdirSync3(path13.dirname(resolved), { recursive: true });
|
|
2322
|
+
writeFileSync3(resolved, rendered, "utf8");
|
|
2323
|
+
return resolved;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// src/commands/sourceCommand.ts
|
|
2327
|
+
function registerSourceCommand(program2) {
|
|
2328
|
+
program2.command("source").description("Retrieve bounded source from an indexed project.").option("--index <dir>", "index artifact directory", ".my-dev-kit-v1").option("--node <node-id>", "node id to retrieve source for").option("--file <path>", "file path").option("--start <n>", "start line", parseInteger).option("--end <n>", "end line", parseInteger).option("--symbol <name>", "symbol name").option("--max-lines <n>", "maximum returned lines", parseInteger, 160).option("--format <json|plain|numbered>", "output format").option("--out <path>", "write output to file").option("--json", "print JSON output (alias for --format json)").action((options) => {
|
|
2329
|
+
const format = resolveFormat(options);
|
|
2330
|
+
const mode = selectMode(options);
|
|
2331
|
+
const artifacts = loadSourceArtifacts({
|
|
2332
|
+
indexDir: options.index,
|
|
2333
|
+
loadCodeGraph: mode === "node",
|
|
2334
|
+
loadSymbolIndex: mode === "symbol" || mode === "node"
|
|
2335
|
+
});
|
|
2336
|
+
let target;
|
|
2337
|
+
if (mode === "line-range") {
|
|
2338
|
+
target = {
|
|
2339
|
+
mode,
|
|
2340
|
+
filePath: options.file,
|
|
2341
|
+
startLine: options.start,
|
|
2342
|
+
endLine: options.end,
|
|
2343
|
+
warnings: []
|
|
2344
|
+
};
|
|
2345
|
+
} else if (mode === "symbol") {
|
|
2346
|
+
target = resolveSymbolTarget(artifacts.symbolIndex, options.file, options.symbol, options.maxLines);
|
|
2347
|
+
} else {
|
|
2348
|
+
const nodeTarget = resolveFileNodeTarget(artifacts.codeGraph, options.node, options.maxLines);
|
|
2349
|
+
target = nodeTarget.mode === "symbol" ? resolveSymbolTarget(artifacts.symbolIndex, nodeTarget.filePath, nodeTarget.symbolName, options.maxLines) : nodeTarget;
|
|
2350
|
+
}
|
|
2351
|
+
const result = getSourceSlice({
|
|
2352
|
+
indexDir: options.index,
|
|
2353
|
+
projectRoot: artifacts.resolved.manifest.projectRoot,
|
|
2354
|
+
filePath: target.filePath,
|
|
2355
|
+
startLine: target.startLine,
|
|
2356
|
+
endLine: target.endLine,
|
|
2357
|
+
maxLines: options.maxLines,
|
|
2358
|
+
mode,
|
|
2359
|
+
symbolName: target.symbolName,
|
|
2360
|
+
warnings: target.warnings
|
|
2361
|
+
});
|
|
2362
|
+
if (format === void 0) {
|
|
2363
|
+
if (options.out) {
|
|
2364
|
+
const rendered2 = renderSourceOutput(result, "plain");
|
|
2365
|
+
const writtenPath = writeSourceOutput(options.out, rendered2);
|
|
2366
|
+
console.log(`Wrote plain source to ${writtenPath}`);
|
|
2367
|
+
} else {
|
|
2368
|
+
console.log(`${result.filePath}:${result.startLine}-${result.endLine}`);
|
|
2369
|
+
if (result.warnings.length > 0) console.log(`Warnings: ${result.warnings.join("; ")}`);
|
|
2370
|
+
console.log(result.content);
|
|
2371
|
+
}
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
const rendered = renderSourceOutput(result, format);
|
|
2375
|
+
if (options.out) {
|
|
2376
|
+
const writtenPath = writeSourceOutput(options.out, rendered);
|
|
2377
|
+
if (format === "json") {
|
|
2378
|
+
console.log(`Wrote JSON source result to ${writtenPath}`);
|
|
2379
|
+
} else {
|
|
2380
|
+
console.log(`Wrote ${format} source to ${writtenPath}`);
|
|
2381
|
+
}
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
process.stdout.write(rendered);
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
function resolveFormat(options) {
|
|
2388
|
+
if (options.json && options.format !== void 0 && options.format !== "json") {
|
|
2389
|
+
throw new Error(`--json and --format ${options.format} cannot be used together. Use --format json or omit --json.`);
|
|
2390
|
+
}
|
|
2391
|
+
if (options.json) return "json";
|
|
2392
|
+
if (options.format !== void 0) return parseSourceOutputFormat(options.format);
|
|
2393
|
+
return void 0;
|
|
2394
|
+
}
|
|
2395
|
+
function selectMode(options) {
|
|
2396
|
+
const hasNode = options.node !== void 0;
|
|
2397
|
+
const hasRange = options.file !== void 0 || options.start !== void 0 || options.end !== void 0;
|
|
2398
|
+
const hasSymbol = options.symbol !== void 0;
|
|
2399
|
+
if (hasNode && (hasRange || hasSymbol)) throw new Error("Use only one source mode: --node, --file with --start/--end, or --file with --symbol.");
|
|
2400
|
+
if (!hasNode && !hasRange && !hasSymbol) throw new Error("Provide one source mode: --node, --file with --start/--end, or --file with --symbol.");
|
|
2401
|
+
if (hasNode) return "node";
|
|
2402
|
+
if (hasSymbol) {
|
|
2403
|
+
if (!options.file) throw new Error("Symbol mode requires --file <path> and --symbol <name>.");
|
|
2404
|
+
if (options.start !== void 0 || options.end !== void 0) throw new Error("Do not mix --symbol with --start/--end.");
|
|
2405
|
+
return "symbol";
|
|
2406
|
+
}
|
|
2407
|
+
if (!options.file) throw new Error("Line range mode requires --file <path>.");
|
|
2408
|
+
if (options.start === void 0) throw new Error("--start is required when --end is provided.");
|
|
2409
|
+
if (options.end === void 0) throw new Error("--end is required when --start is provided.");
|
|
2410
|
+
return "line-range";
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// src/commands/viewCommand.ts
|
|
2414
|
+
import * as path15 from "path";
|
|
2415
|
+
|
|
2416
|
+
// src/graph/edgeStyleConvention.ts
|
|
2417
|
+
var KNOWN_EDGE_KINDS = [
|
|
2418
|
+
"defines",
|
|
2419
|
+
"imports",
|
|
2420
|
+
"exports",
|
|
2421
|
+
"calls",
|
|
2422
|
+
"depends-on",
|
|
2423
|
+
"related-to"
|
|
2424
|
+
];
|
|
2425
|
+
var EDGE_ATTR_MAP = {
|
|
2426
|
+
defines: { dir: "both", arrowtail: "dot", arrowhead: "normal", style: "solid" },
|
|
2427
|
+
imports: { dir: "both", arrowtail: "dot", arrowhead: "inv", style: "solid" },
|
|
2428
|
+
exports: { dir: "both", arrowtail: "dot", arrowhead: "onormal", style: "solid" },
|
|
2429
|
+
calls: { dir: "both", arrowtail: "dot", arrowhead: "normal", style: "bold" },
|
|
2430
|
+
"depends-on": { dir: "both", arrowtail: "dot", arrowhead: "inv", style: "dashed" },
|
|
2431
|
+
"related-to": { dir: "both", arrowtail: "odot", arrowhead: "odot", style: "dotted" }
|
|
2432
|
+
};
|
|
2433
|
+
var FALLBACK_ATTRS = { dir: "forward", arrowtail: "none", arrowhead: "normal", style: "solid" };
|
|
2434
|
+
function getEdgeAttrs(kind) {
|
|
2435
|
+
return EDGE_ATTR_MAP[kind] ?? FALLBACK_ATTRS;
|
|
2436
|
+
}
|
|
2437
|
+
function formatEdgeAttrs(attrs) {
|
|
2438
|
+
return `dir="${attrs.dir}", arrowtail="${attrs.arrowtail}", arrowhead="${attrs.arrowhead}", style="${attrs.style}"`;
|
|
2439
|
+
}
|
|
2440
|
+
function buildLegendLines() {
|
|
2441
|
+
const lines = [
|
|
2442
|
+
" subgraph cluster_legend {",
|
|
2443
|
+
' label="Edge Legend"; style=dotted; color=gray;'
|
|
2444
|
+
];
|
|
2445
|
+
for (const kind of KNOWN_EDGE_KINDS) {
|
|
2446
|
+
const src = `legend_${kind.replace("-", "_")}_src`;
|
|
2447
|
+
const tgt = `legend_${kind.replace("-", "_")}_tgt`;
|
|
2448
|
+
const attrs = getEdgeAttrs(kind);
|
|
2449
|
+
lines.push(` "${src}" [label="${kind} (source)", shape=plaintext, fontsize=9];`);
|
|
2450
|
+
lines.push(` "${tgt}" [label="${kind} (target)", shape=plaintext, fontsize=9];`);
|
|
2451
|
+
lines.push(` "${src}" -> "${tgt}" [${formatEdgeAttrs(attrs)}, label="${kind}", fontsize=9];`);
|
|
2452
|
+
}
|
|
2453
|
+
lines.push(" }");
|
|
2454
|
+
return lines;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/graph/buildDotGraph.ts
|
|
2458
|
+
function buildDotGraph(graph, options = {}) {
|
|
2459
|
+
const mode = options.edgeStyle ?? "semantic";
|
|
2460
|
+
const lines = ["digraph CodeGraph {", " rankdir=LR;"];
|
|
2461
|
+
for (const node of [...graph.nodes].sort(compareById3)) {
|
|
2462
|
+
lines.push(` ${quote(node.id)} [label=${quote(nodeLabel(node))}, shape=${quote(nodeShape(node))}];`);
|
|
2463
|
+
}
|
|
2464
|
+
for (const edge of [...graph.edges].sort(compareById3)) {
|
|
2465
|
+
if (mode === "labeled") {
|
|
2466
|
+
lines.push(` ${quote(edge.source)} -> ${quote(edge.target)} [label=${quote(edge.label ?? edge.kind)}];`);
|
|
2467
|
+
} else if (mode === "minimal") {
|
|
2468
|
+
lines.push(` ${quote(edge.source)} -> ${quote(edge.target)};`);
|
|
2469
|
+
} else {
|
|
2470
|
+
const attrs = getEdgeAttrs(edge.kind);
|
|
2471
|
+
lines.push(` ${quote(edge.source)} -> ${quote(edge.target)} [${formatEdgeAttrs(attrs)}];`);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
if (mode === "semantic") {
|
|
2475
|
+
lines.push(...buildLegendLines());
|
|
2476
|
+
}
|
|
2477
|
+
lines.push("}");
|
|
2478
|
+
return `${lines.join("\n")}
|
|
2479
|
+
`;
|
|
2480
|
+
}
|
|
2481
|
+
function nodeLabel(node) {
|
|
2482
|
+
if (node.kind === "file") return node.path ?? node.label;
|
|
2483
|
+
if (node.kind === "symbol") return node.symbolName ?? node.label;
|
|
2484
|
+
return node.label;
|
|
2485
|
+
}
|
|
2486
|
+
function nodeShape(node) {
|
|
2487
|
+
if (node.kind === "file") return "box";
|
|
2488
|
+
if (node.kind === "symbol") return "ellipse";
|
|
2489
|
+
return "oval";
|
|
2490
|
+
}
|
|
2491
|
+
function quote(value) {
|
|
2492
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, "\\n")}"`;
|
|
2493
|
+
}
|
|
2494
|
+
function compareById3(a, b) {
|
|
2495
|
+
return a.id.localeCompare(b.id);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// src/graph/renderGraphviz.ts
|
|
2499
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2500
|
+
function isGraphvizAvailable() {
|
|
2501
|
+
const result = spawnSync2("dot", ["-V"], { encoding: "utf8", shell: false });
|
|
2502
|
+
return result.status === 0;
|
|
2503
|
+
}
|
|
2504
|
+
function renderGraphviz(dotText, format) {
|
|
2505
|
+
const result = spawnSync2("dot", [`-T${format}`], {
|
|
2506
|
+
input: dotText,
|
|
2507
|
+
encoding: format === "svg" ? "utf8" : "buffer",
|
|
2508
|
+
shell: false,
|
|
2509
|
+
maxBuffer: 20 * 1024 * 1024
|
|
2510
|
+
});
|
|
2511
|
+
if (result.error) throw new Error(`Graphviz dot failed: ${result.error.message}`);
|
|
2512
|
+
if (result.status !== 0) {
|
|
2513
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : result.stderr?.toString("utf8");
|
|
2514
|
+
throw new Error(`Graphviz dot failed with exit code ${result.status}: ${stderr ?? ""}`.trim());
|
|
2515
|
+
}
|
|
2516
|
+
return Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout, "utf8");
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// src/graph/writeGraphView.ts
|
|
2520
|
+
import * as fs10 from "fs";
|
|
2521
|
+
import * as path14 from "path";
|
|
2522
|
+
function writeGraphView(outputPath, content) {
|
|
2523
|
+
const resolved = path14.resolve(outputPath);
|
|
2524
|
+
fs10.mkdirSync(path14.dirname(resolved), { recursive: true });
|
|
2525
|
+
fs10.writeFileSync(resolved, content);
|
|
2526
|
+
return resolved.replace(/\\/g, "/");
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// src/commands/viewCommand.ts
|
|
2530
|
+
function registerViewCommand(program2) {
|
|
2531
|
+
program2.command("view").description("Render code graph artifacts as DOT, SVG, or PNG.").option("--index <dir>", "index artifact directory", ".my-dev-kit-v1").option("--format <dot|svg|png>", "output format", "dot").option("--out <path>", "output path").option("--edge-style <semantic|labeled|minimal>", "edge visualization style", "semantic").option("--allow-dot-fallback", "fall back when Graphviz dot is unavailable").option("--json", "print JSON output").action((options) => {
|
|
2532
|
+
const requestedFormat = parseFormat(options.format);
|
|
2533
|
+
const edgeStyle = parseEdgeStyle(options.edgeStyle);
|
|
2534
|
+
const artifacts = loadLookupArtifacts(options.index);
|
|
2535
|
+
const dotText = buildDotGraph(artifacts.codeGraph, { edgeStyle });
|
|
2536
|
+
const warnings = [];
|
|
2537
|
+
let actualFormat = requestedFormat;
|
|
2538
|
+
let graphvizUsed = false;
|
|
2539
|
+
let dotFallbackUsed = false;
|
|
2540
|
+
let outputPath = options.out ?? path15.join(options.index, `graph.${requestedFormat}`);
|
|
2541
|
+
let content = dotText;
|
|
2542
|
+
if (requestedFormat !== "dot") {
|
|
2543
|
+
if (!isGraphvizAvailable()) {
|
|
2544
|
+
if (!options.allowDotFallback) {
|
|
2545
|
+
throw new Error("Graphviz dot executable is not available. Install Graphviz or use --allow-dot-fallback.");
|
|
2546
|
+
}
|
|
2547
|
+
warnings.push("Graphviz dot executable is not available; wrote DOT fallback instead.");
|
|
2548
|
+
actualFormat = "dot";
|
|
2549
|
+
dotFallbackUsed = true;
|
|
2550
|
+
outputPath = options.out ?? path15.join(options.index, "graph.dot");
|
|
2551
|
+
if (!outputPath.endsWith(".dot")) outputPath = outputPath.replace(/\.(svg|png)$/i, ".dot");
|
|
2552
|
+
} else {
|
|
2553
|
+
content = renderGraphviz(dotText, requestedFormat);
|
|
2554
|
+
graphvizUsed = true;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
const writtenPath = writeGraphView(outputPath, content);
|
|
2558
|
+
const result = {
|
|
2559
|
+
status: "ok",
|
|
2560
|
+
indexDir: options.index,
|
|
2561
|
+
requestedFormat,
|
|
2562
|
+
actualFormat,
|
|
2563
|
+
outputPath: writtenPath,
|
|
2564
|
+
nodeCount: artifacts.codeGraph.nodes.length,
|
|
2565
|
+
edgeCount: artifacts.codeGraph.edges.length,
|
|
2566
|
+
graphvizUsed,
|
|
2567
|
+
dotFallbackUsed,
|
|
2568
|
+
edgeStyle,
|
|
2569
|
+
warnings
|
|
2570
|
+
};
|
|
2571
|
+
if (options.json) {
|
|
2572
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
console.log(`Wrote ${actualFormat.toUpperCase()} graph: ${writtenPath}`);
|
|
2576
|
+
if (warnings.length > 0) console.log(`Warnings: ${warnings.join("; ")}`);
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
function parseFormat(format) {
|
|
2580
|
+
if (format === "dot" || format === "svg" || format === "png") return format;
|
|
2581
|
+
throw new Error(`Unsupported view format "${format}". Supported values: dot, svg, png.`);
|
|
2582
|
+
}
|
|
2583
|
+
function parseEdgeStyle(value) {
|
|
2584
|
+
if (value === "semantic" || value === "labeled" || value === "minimal") return value;
|
|
2585
|
+
throw new Error(`Unsupported --edge-style value "${value}". Supported values: semantic, labeled, minimal.`);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// src/version.ts
|
|
2589
|
+
var VERSION = "1.0.0";
|
|
2590
|
+
|
|
2591
|
+
// src/cli.ts
|
|
2592
|
+
function createProgram() {
|
|
2593
|
+
const program2 = new Command();
|
|
2594
|
+
program2.name("my-dev-kit").description("Local codebase graph, indexing, slicing, source retrieval, search, and Graphviz visualization CLI.").version(VERSION);
|
|
2595
|
+
registerIndexCommand(program2);
|
|
2596
|
+
registerViewCommand(program2);
|
|
2597
|
+
registerLookupCommand(program2);
|
|
2598
|
+
registerSourceCommand(program2);
|
|
2599
|
+
registerSliceCommand(program2);
|
|
2600
|
+
registerSearchCommand(program2);
|
|
2601
|
+
return program2;
|
|
2602
|
+
}
|
|
2603
|
+
var program = createProgram();
|
|
2604
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
2605
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2606
|
+
console.error(message);
|
|
2607
|
+
process.exitCode = 2;
|
|
2608
|
+
});
|
|
2609
|
+
export {
|
|
2610
|
+
createProgram
|
|
2611
|
+
};
|