ctxo-mcp 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -0
- package/dist/chunk-JIDIH7DS.js +28 -0
- package/dist/{chunk-54ETLIQX.js → chunk-N6GPODUY.js} +8 -4
- package/dist/chunk-N6GPODUY.js.map +1 -0
- package/dist/{chunk-P7JUSY3I.js → chunk-XSHNN6PU.js} +167 -16
- package/dist/chunk-XSHNN6PU.js.map +1 -0
- package/dist/{cli-router-PIWHLS5F.js → cli-router-NRUGPICL.js} +808 -69
- package/dist/cli-router-NRUGPICL.js.map +1 -0
- package/dist/index.js +1643 -66
- package/dist/index.js.map +1 -1
- package/dist/json-index-reader-FCKSKA6R.js +9 -0
- package/dist/json-index-reader-FCKSKA6R.js.map +1 -0
- package/dist/{staleness-detector-5AN223FM.js → staleness-detector-VSDPTPX7.js} +2 -1
- package/dist/{staleness-detector-5AN223FM.js.map → staleness-detector-VSDPTPX7.js.map} +1 -1
- package/llms-full.txt +260 -0
- package/llms.txt +53 -0
- package/package.json +7 -2
- package/dist/chunk-54ETLIQX.js.map +0 -1
- package/dist/chunk-P7JUSY3I.js.map +0 -1
- package/dist/cli-router-PIWHLS5F.js.map +0 -1
- package/dist/json-index-reader-PNLPAS42.js +0 -8
- /package/dist/{json-index-reader-PNLPAS42.js.map → chunk-JIDIH7DS.js.map} +0 -0
|
@@ -2,16 +2,520 @@
|
|
|
2
2
|
import {
|
|
3
3
|
RevertDetector,
|
|
4
4
|
SimpleGitAdapter,
|
|
5
|
-
SqliteStorageAdapter
|
|
6
|
-
|
|
5
|
+
SqliteStorageAdapter,
|
|
6
|
+
aggregateCoChanges
|
|
7
|
+
} from "./chunk-XSHNN6PU.js";
|
|
7
8
|
import {
|
|
8
|
-
JsonIndexReader
|
|
9
|
-
|
|
9
|
+
JsonIndexReader,
|
|
10
|
+
SYMBOL_KINDS
|
|
11
|
+
} from "./chunk-N6GPODUY.js";
|
|
12
|
+
import {
|
|
13
|
+
__esm,
|
|
14
|
+
__export,
|
|
15
|
+
__toCommonJS
|
|
16
|
+
} from "./chunk-JIDIH7DS.js";
|
|
17
|
+
|
|
18
|
+
// src/adapters/language/tree-sitter-adapter.ts
|
|
19
|
+
import Parser from "tree-sitter";
|
|
20
|
+
import { extname as extname3 } from "path";
|
|
21
|
+
var TreeSitterAdapter;
|
|
22
|
+
var init_tree_sitter_adapter = __esm({
|
|
23
|
+
"src/adapters/language/tree-sitter-adapter.ts"() {
|
|
24
|
+
"use strict";
|
|
25
|
+
TreeSitterAdapter = class {
|
|
26
|
+
tier = "syntax";
|
|
27
|
+
parser;
|
|
28
|
+
symbolRegistry = /* @__PURE__ */ new Map();
|
|
29
|
+
constructor(language) {
|
|
30
|
+
this.parser = new Parser();
|
|
31
|
+
this.parser.setLanguage(language);
|
|
32
|
+
}
|
|
33
|
+
isSupported(filePath) {
|
|
34
|
+
const ext = extname3(filePath).toLowerCase();
|
|
35
|
+
return this.extensions.includes(ext);
|
|
36
|
+
}
|
|
37
|
+
setSymbolRegistry(registry) {
|
|
38
|
+
this.symbolRegistry = registry;
|
|
39
|
+
}
|
|
40
|
+
parse(source) {
|
|
41
|
+
return this.parser.parse(source);
|
|
42
|
+
}
|
|
43
|
+
buildSymbolId(filePath, name, kind) {
|
|
44
|
+
return `${filePath}::${name}::${kind}`;
|
|
45
|
+
}
|
|
46
|
+
nodeToLineRange(node) {
|
|
47
|
+
return {
|
|
48
|
+
startLine: node.startPosition.row,
|
|
49
|
+
endLine: node.endPosition.row,
|
|
50
|
+
startOffset: node.startIndex,
|
|
51
|
+
endOffset: node.endIndex
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
countCyclomaticComplexity(node, branchTypes) {
|
|
55
|
+
let complexity = 1;
|
|
56
|
+
const visit = (n) => {
|
|
57
|
+
if (branchTypes.includes(n.type)) {
|
|
58
|
+
complexity++;
|
|
59
|
+
}
|
|
60
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
61
|
+
visit(n.child(i));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
visit(node);
|
|
65
|
+
return complexity;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// src/adapters/language/go-adapter.ts
|
|
72
|
+
var go_adapter_exports = {};
|
|
73
|
+
__export(go_adapter_exports, {
|
|
74
|
+
GoAdapter: () => GoAdapter
|
|
75
|
+
});
|
|
76
|
+
import GoLanguage from "tree-sitter-go";
|
|
77
|
+
var GO_BRANCH_TYPES, GoAdapter;
|
|
78
|
+
var init_go_adapter = __esm({
|
|
79
|
+
"src/adapters/language/go-adapter.ts"() {
|
|
80
|
+
"use strict";
|
|
81
|
+
init_tree_sitter_adapter();
|
|
82
|
+
GO_BRANCH_TYPES = [
|
|
83
|
+
"if_statement",
|
|
84
|
+
"for_statement",
|
|
85
|
+
"expression_switch_statement",
|
|
86
|
+
"type_switch_statement",
|
|
87
|
+
"expression_case",
|
|
88
|
+
"type_case",
|
|
89
|
+
"select_statement",
|
|
90
|
+
"communication_case"
|
|
91
|
+
];
|
|
92
|
+
GoAdapter = class extends TreeSitterAdapter {
|
|
93
|
+
extensions = [".go"];
|
|
94
|
+
constructor() {
|
|
95
|
+
super(GoLanguage);
|
|
96
|
+
}
|
|
97
|
+
extractSymbols(filePath, source) {
|
|
98
|
+
try {
|
|
99
|
+
const tree = this.parse(source);
|
|
100
|
+
const symbols = [];
|
|
101
|
+
for (let i = 0; i < tree.rootNode.childCount; i++) {
|
|
102
|
+
const node = tree.rootNode.child(i);
|
|
103
|
+
if (node.type === "function_declaration") {
|
|
104
|
+
const name = node.childForFieldName("name")?.text;
|
|
105
|
+
if (!name || !this.isExported(name)) continue;
|
|
106
|
+
const range = this.nodeToLineRange(node);
|
|
107
|
+
symbols.push({
|
|
108
|
+
symbolId: this.buildSymbolId(filePath, name, "function"),
|
|
109
|
+
name,
|
|
110
|
+
kind: "function",
|
|
111
|
+
...range
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (node.type === "method_declaration") {
|
|
115
|
+
const methodName = node.childForFieldName("name")?.text;
|
|
116
|
+
if (!methodName || !this.isExported(methodName)) continue;
|
|
117
|
+
const receiverType = this.extractReceiverType(node);
|
|
118
|
+
const qualifiedName = receiverType ? `${receiverType}.${methodName}` : methodName;
|
|
119
|
+
const range = this.nodeToLineRange(node);
|
|
120
|
+
symbols.push({
|
|
121
|
+
symbolId: this.buildSymbolId(filePath, qualifiedName, "method"),
|
|
122
|
+
name: qualifiedName,
|
|
123
|
+
kind: "method",
|
|
124
|
+
...range
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (node.type === "type_declaration") {
|
|
128
|
+
this.extractTypeSymbols(node, filePath, symbols);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return symbols;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(`[ctxo:go] Symbol extraction failed for ${filePath}: ${err.message}`);
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
extractEdges(filePath, source) {
|
|
138
|
+
try {
|
|
139
|
+
const tree = this.parse(source);
|
|
140
|
+
const edges = [];
|
|
141
|
+
const firstExportedSymbol = this.findFirstExportedSymbolId(tree.rootNode, filePath);
|
|
142
|
+
if (!firstExportedSymbol) return edges;
|
|
143
|
+
for (let i = 0; i < tree.rootNode.childCount; i++) {
|
|
144
|
+
const node = tree.rootNode.child(i);
|
|
145
|
+
if (node.type === "import_declaration") {
|
|
146
|
+
this.extractImportEdges(node, filePath, firstExportedSymbol, edges);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return edges;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(`[ctxo:go] Edge extraction failed for ${filePath}: ${err.message}`);
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
extractComplexity(filePath, source) {
|
|
156
|
+
try {
|
|
157
|
+
const tree = this.parse(source);
|
|
158
|
+
const metrics = [];
|
|
159
|
+
for (let i = 0; i < tree.rootNode.childCount; i++) {
|
|
160
|
+
const node = tree.rootNode.child(i);
|
|
161
|
+
if (node.type === "function_declaration") {
|
|
162
|
+
const name = node.childForFieldName("name")?.text;
|
|
163
|
+
if (!name || !this.isExported(name)) continue;
|
|
164
|
+
metrics.push({
|
|
165
|
+
symbolId: this.buildSymbolId(filePath, name, "function"),
|
|
166
|
+
cyclomatic: this.countCyclomaticComplexity(node, GO_BRANCH_TYPES)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (node.type === "method_declaration") {
|
|
170
|
+
const methodName = node.childForFieldName("name")?.text;
|
|
171
|
+
if (!methodName || !this.isExported(methodName)) continue;
|
|
172
|
+
const receiverType = this.extractReceiverType(node);
|
|
173
|
+
const qualifiedName = receiverType ? `${receiverType}.${methodName}` : methodName;
|
|
174
|
+
metrics.push({
|
|
175
|
+
symbolId: this.buildSymbolId(filePath, qualifiedName, "method"),
|
|
176
|
+
cyclomatic: this.countCyclomaticComplexity(node, GO_BRANCH_TYPES)
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return metrics;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(`[ctxo:go] Complexity extraction failed for ${filePath}: ${err.message}`);
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ── Private helpers ─────────────────────────────────────────
|
|
187
|
+
isExported(name) {
|
|
188
|
+
return name.length > 0 && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase();
|
|
189
|
+
}
|
|
190
|
+
extractReceiverType(methodNode) {
|
|
191
|
+
const params = methodNode.child(1);
|
|
192
|
+
if (params?.type !== "parameter_list") return void 0;
|
|
193
|
+
for (let i = 0; i < params.childCount; i++) {
|
|
194
|
+
const param = params.child(i);
|
|
195
|
+
if (param.type === "parameter_declaration") {
|
|
196
|
+
const typeNode = param.childForFieldName("type");
|
|
197
|
+
if (typeNode) {
|
|
198
|
+
const text = typeNode.text;
|
|
199
|
+
return text.replace(/^\*/, "");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return void 0;
|
|
204
|
+
}
|
|
205
|
+
extractTypeSymbols(typeDecl, filePath, symbols) {
|
|
206
|
+
for (let i = 0; i < typeDecl.childCount; i++) {
|
|
207
|
+
const spec = typeDecl.child(i);
|
|
208
|
+
if (spec.type !== "type_spec") continue;
|
|
209
|
+
const name = spec.childForFieldName("name")?.text;
|
|
210
|
+
if (!name || !this.isExported(name)) continue;
|
|
211
|
+
const typeBody = spec.childForFieldName("type");
|
|
212
|
+
let kind = "type";
|
|
213
|
+
if (typeBody?.type === "struct_type") kind = "class";
|
|
214
|
+
else if (typeBody?.type === "interface_type") kind = "interface";
|
|
215
|
+
const range = this.nodeToLineRange(spec);
|
|
216
|
+
symbols.push({
|
|
217
|
+
symbolId: this.buildSymbolId(filePath, name, kind),
|
|
218
|
+
name,
|
|
219
|
+
kind,
|
|
220
|
+
...range
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
extractImportEdges(importDecl, _filePath, fromSymbol, edges) {
|
|
225
|
+
const visit = (node) => {
|
|
226
|
+
if (node.type === "import_spec") {
|
|
227
|
+
const pathNode = node.childForFieldName("path") ?? node.child(0);
|
|
228
|
+
if (pathNode) {
|
|
229
|
+
const importPath = pathNode.text.replace(/"/g, "");
|
|
230
|
+
edges.push({
|
|
231
|
+
from: fromSymbol,
|
|
232
|
+
to: `${importPath}::${importPath.split("/").pop()}::variable`,
|
|
233
|
+
kind: "imports"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
238
|
+
visit(node.child(i));
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
visit(importDecl);
|
|
242
|
+
}
|
|
243
|
+
findFirstExportedSymbolId(rootNode, filePath) {
|
|
244
|
+
for (let i = 0; i < rootNode.childCount; i++) {
|
|
245
|
+
const node = rootNode.child(i);
|
|
246
|
+
if (node.type === "function_declaration") {
|
|
247
|
+
const name = node.childForFieldName("name")?.text;
|
|
248
|
+
if (name && this.isExported(name)) return this.buildSymbolId(filePath, name, "function");
|
|
249
|
+
}
|
|
250
|
+
if (node.type === "type_declaration") {
|
|
251
|
+
for (let j = 0; j < node.childCount; j++) {
|
|
252
|
+
const spec = node.child(j);
|
|
253
|
+
if (spec.type === "type_spec") {
|
|
254
|
+
const name = spec.childForFieldName("name")?.text;
|
|
255
|
+
if (name && this.isExported(name)) {
|
|
256
|
+
const typeBody = spec.childForFieldName("type");
|
|
257
|
+
const kind = typeBody?.type === "struct_type" ? "class" : typeBody?.type === "interface_type" ? "interface" : "type";
|
|
258
|
+
return this.buildSymbolId(filePath, name, kind);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// src/adapters/language/csharp-adapter.ts
|
|
271
|
+
var csharp_adapter_exports = {};
|
|
272
|
+
__export(csharp_adapter_exports, {
|
|
273
|
+
CSharpAdapter: () => CSharpAdapter
|
|
274
|
+
});
|
|
275
|
+
import CSharpLanguage from "tree-sitter-c-sharp";
|
|
276
|
+
var CSHARP_BRANCH_TYPES, CSharpAdapter;
|
|
277
|
+
var init_csharp_adapter = __esm({
|
|
278
|
+
"src/adapters/language/csharp-adapter.ts"() {
|
|
279
|
+
"use strict";
|
|
280
|
+
init_tree_sitter_adapter();
|
|
281
|
+
CSHARP_BRANCH_TYPES = [
|
|
282
|
+
"if_statement",
|
|
283
|
+
"for_statement",
|
|
284
|
+
"foreach_statement",
|
|
285
|
+
"while_statement",
|
|
286
|
+
"do_statement",
|
|
287
|
+
"switch_section",
|
|
288
|
+
"catch_clause",
|
|
289
|
+
"conditional_expression"
|
|
290
|
+
];
|
|
291
|
+
CSharpAdapter = class extends TreeSitterAdapter {
|
|
292
|
+
extensions = [".cs"];
|
|
293
|
+
constructor() {
|
|
294
|
+
super(CSharpLanguage);
|
|
295
|
+
}
|
|
296
|
+
extractSymbols(filePath, source) {
|
|
297
|
+
try {
|
|
298
|
+
const tree = this.parse(source);
|
|
299
|
+
const symbols = [];
|
|
300
|
+
this.visitSymbols(tree.rootNode, filePath, "", symbols);
|
|
301
|
+
return symbols;
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.error(`[ctxo:csharp] Symbol extraction failed for ${filePath}: ${err.message}`);
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
extractEdges(filePath, source) {
|
|
308
|
+
try {
|
|
309
|
+
const tree = this.parse(source);
|
|
310
|
+
const edges = [];
|
|
311
|
+
const symbols = this.extractSymbols(filePath, source);
|
|
312
|
+
const firstSymbol = symbols.length > 0 ? symbols[0].symbolId : void 0;
|
|
313
|
+
if (!firstSymbol) return edges;
|
|
314
|
+
this.visitEdges(tree.rootNode, filePath, firstSymbol, "", edges);
|
|
315
|
+
return edges;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.error(`[ctxo:csharp] Edge extraction failed for ${filePath}: ${err.message}`);
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
extractComplexity(filePath, source) {
|
|
322
|
+
try {
|
|
323
|
+
const tree = this.parse(source);
|
|
324
|
+
const metrics = [];
|
|
325
|
+
this.visitComplexity(tree.rootNode, filePath, "", metrics);
|
|
326
|
+
return metrics;
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.error(`[ctxo:csharp] Complexity extraction failed for ${filePath}: ${err.message}`);
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// ── Symbol visitor ──────────────────────────────────────────
|
|
333
|
+
visitSymbols(node, filePath, namespace, symbols) {
|
|
334
|
+
if (node.type === "namespace_declaration") {
|
|
335
|
+
const name = node.childForFieldName("name")?.text ?? "";
|
|
336
|
+
const ns = namespace ? `${namespace}.${name}` : name;
|
|
337
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
338
|
+
this.visitSymbols(node.child(i), filePath, ns, symbols);
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (node.type === "declaration_list") {
|
|
343
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
344
|
+
this.visitSymbols(node.child(i), filePath, namespace, symbols);
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const typeMapping = {
|
|
349
|
+
class_declaration: "class",
|
|
350
|
+
struct_declaration: "class",
|
|
351
|
+
record_declaration: "class",
|
|
352
|
+
interface_declaration: "interface",
|
|
353
|
+
enum_declaration: "type"
|
|
354
|
+
};
|
|
355
|
+
const kind = typeMapping[node.type];
|
|
356
|
+
if (kind) {
|
|
357
|
+
if (!this.isPublic(node)) return;
|
|
358
|
+
const name = node.childForFieldName("name")?.text;
|
|
359
|
+
if (!name) return;
|
|
360
|
+
const qualifiedName = namespace ? `${namespace}.${name}` : name;
|
|
361
|
+
const range = this.nodeToLineRange(node);
|
|
362
|
+
symbols.push({
|
|
363
|
+
symbolId: this.buildSymbolId(filePath, qualifiedName, kind),
|
|
364
|
+
name: qualifiedName,
|
|
365
|
+
kind,
|
|
366
|
+
...range
|
|
367
|
+
});
|
|
368
|
+
if (kind === "class") {
|
|
369
|
+
this.extractMethodSymbols(node, filePath, qualifiedName, symbols);
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
374
|
+
this.visitSymbols(node.child(i), filePath, namespace, symbols);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
extractMethodSymbols(classNode, filePath, className, symbols) {
|
|
378
|
+
const declList = classNode.children.find((c) => c.type === "declaration_list");
|
|
379
|
+
if (!declList) return;
|
|
380
|
+
for (let i = 0; i < declList.childCount; i++) {
|
|
381
|
+
const child = declList.child(i);
|
|
382
|
+
if (child.type !== "method_declaration" && child.type !== "constructor_declaration") continue;
|
|
383
|
+
if (!this.isPublic(child)) continue;
|
|
384
|
+
const name = child.childForFieldName("name")?.text;
|
|
385
|
+
if (!name) continue;
|
|
386
|
+
const paramCount = this.countParameters(child);
|
|
387
|
+
const qualifiedName = `${className}.${name}(${paramCount})`;
|
|
388
|
+
const range = this.nodeToLineRange(child);
|
|
389
|
+
symbols.push({
|
|
390
|
+
symbolId: this.buildSymbolId(filePath, qualifiedName, "method"),
|
|
391
|
+
name: qualifiedName,
|
|
392
|
+
kind: "method",
|
|
393
|
+
...range
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// ── Edge visitor ────────────────────────────────────────────
|
|
398
|
+
visitEdges(node, filePath, fromSymbol, namespace, edges) {
|
|
399
|
+
if (node.type === "using_directive") {
|
|
400
|
+
const nameNode = node.children.find((c) => c.type === "identifier" || c.type === "qualified_name");
|
|
401
|
+
if (nameNode) {
|
|
402
|
+
edges.push({
|
|
403
|
+
from: fromSymbol,
|
|
404
|
+
to: `${nameNode.text}::${nameNode.text.split(".").pop()}::variable`,
|
|
405
|
+
kind: "imports"
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (node.type === "namespace_declaration") {
|
|
411
|
+
const name = node.childForFieldName("name")?.text ?? "";
|
|
412
|
+
const ns = namespace ? `${namespace}.${name}` : name;
|
|
413
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
414
|
+
this.visitEdges(node.child(i), filePath, fromSymbol, ns, edges);
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (node.type === "class_declaration" || node.type === "struct_declaration") {
|
|
419
|
+
if (!this.isPublic(node)) return;
|
|
420
|
+
const name = node.childForFieldName("name")?.text;
|
|
421
|
+
if (!name) return;
|
|
422
|
+
const qualifiedName = namespace ? `${namespace}.${name}` : name;
|
|
423
|
+
const classSymbolId = this.buildSymbolId(filePath, qualifiedName, "class");
|
|
424
|
+
const baseList = node.children.find((c) => c.type === "base_list");
|
|
425
|
+
if (baseList) {
|
|
426
|
+
for (let i = 0; i < baseList.childCount; i++) {
|
|
427
|
+
const child = baseList.child(i);
|
|
428
|
+
if (child.type === "identifier" || child.type === "qualified_name") {
|
|
429
|
+
const baseName = child.text;
|
|
430
|
+
const edgeKind = baseName.match(/^I[A-Z]/) ? "implements" : "extends";
|
|
431
|
+
const targetKind = edgeKind === "implements" ? "interface" : "class";
|
|
432
|
+
edges.push({
|
|
433
|
+
from: classSymbolId,
|
|
434
|
+
to: this.resolveBaseType(baseName, namespace, targetKind),
|
|
435
|
+
kind: edgeKind
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
442
|
+
this.visitEdges(node.child(i), filePath, fromSymbol, namespace, edges);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// ── Complexity visitor ──────────────────────────────────────
|
|
446
|
+
visitComplexity(node, filePath, namespace, metrics) {
|
|
447
|
+
if (node.type === "namespace_declaration") {
|
|
448
|
+
const name = node.childForFieldName("name")?.text ?? "";
|
|
449
|
+
const ns = namespace ? `${namespace}.${name}` : name;
|
|
450
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
451
|
+
this.visitComplexity(node.child(i), filePath, ns, metrics);
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const typeMapping = {
|
|
456
|
+
class_declaration: true,
|
|
457
|
+
struct_declaration: true,
|
|
458
|
+
record_declaration: true
|
|
459
|
+
};
|
|
460
|
+
if (typeMapping[node.type]) {
|
|
461
|
+
if (!this.isPublic(node)) return;
|
|
462
|
+
const className = node.childForFieldName("name")?.text;
|
|
463
|
+
if (!className) return;
|
|
464
|
+
const qualifiedClass = namespace ? `${namespace}.${className}` : className;
|
|
465
|
+
const declList = node.children.find((c) => c.type === "declaration_list");
|
|
466
|
+
if (!declList) return;
|
|
467
|
+
for (let i = 0; i < declList.childCount; i++) {
|
|
468
|
+
const child = declList.child(i);
|
|
469
|
+
if (child.type !== "method_declaration") continue;
|
|
470
|
+
if (!this.isPublic(child)) continue;
|
|
471
|
+
const methodName = child.childForFieldName("name")?.text;
|
|
472
|
+
if (!methodName) continue;
|
|
473
|
+
const paramCount = this.countParameters(child);
|
|
474
|
+
metrics.push({
|
|
475
|
+
symbolId: this.buildSymbolId(filePath, `${qualifiedClass}.${methodName}(${paramCount})`, "method"),
|
|
476
|
+
cyclomatic: this.countCyclomaticComplexity(child, CSHARP_BRANCH_TYPES)
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
482
|
+
this.visitComplexity(node.child(i), filePath, namespace, metrics);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// ── Helpers ─────────────────────────────────────────────────
|
|
486
|
+
countParameters(methodNode) {
|
|
487
|
+
const paramList = methodNode.childForFieldName("parameters");
|
|
488
|
+
if (!paramList) return 0;
|
|
489
|
+
let count = 0;
|
|
490
|
+
for (let i = 0; i < paramList.childCount; i++) {
|
|
491
|
+
if (paramList.child(i).type === "parameter") count++;
|
|
492
|
+
}
|
|
493
|
+
return count;
|
|
494
|
+
}
|
|
495
|
+
isPublic(node) {
|
|
496
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
497
|
+
const child = node.child(i);
|
|
498
|
+
if (child.type === "modifier" && child.text === "public") return true;
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
resolveBaseType(baseName, namespace, defaultKind) {
|
|
503
|
+
const prefix = namespace ? `${namespace}.${baseName}` : baseName;
|
|
504
|
+
for (const [id] of this.symbolRegistry) {
|
|
505
|
+
if (id.includes(`::${prefix}::`)) return id;
|
|
506
|
+
if (id.includes(`::${baseName}::`)) return id;
|
|
507
|
+
}
|
|
508
|
+
const qualifiedName = namespace ? `${namespace}.${baseName}` : baseName;
|
|
509
|
+
return `${qualifiedName}::${baseName}::${defaultKind}`;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
});
|
|
10
514
|
|
|
11
515
|
// src/cli/index-command.ts
|
|
12
516
|
import { execFileSync } from "child_process";
|
|
13
517
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync3, statSync, readdirSync } from "fs";
|
|
14
|
-
import { join as join4, relative, extname as
|
|
518
|
+
import { join as join4, relative, extname as extname4 } from "path";
|
|
15
519
|
|
|
16
520
|
// src/core/staleness/content-hasher.ts
|
|
17
521
|
import { createHash } from "crypto";
|
|
@@ -34,6 +538,8 @@ var TsMorphAdapter = class {
|
|
|
34
538
|
extensions = SUPPORTED_EXTENSIONS;
|
|
35
539
|
tier = "full";
|
|
36
540
|
project;
|
|
541
|
+
symbolRegistry = /* @__PURE__ */ new Map();
|
|
542
|
+
projectPreloaded = false;
|
|
37
543
|
constructor() {
|
|
38
544
|
this.project = new Project({
|
|
39
545
|
compilerOptions: {
|
|
@@ -50,6 +556,27 @@ var TsMorphAdapter = class {
|
|
|
50
556
|
const ext = extname(filePath).toLowerCase();
|
|
51
557
|
return SUPPORTED_EXTENSIONS.includes(ext);
|
|
52
558
|
}
|
|
559
|
+
setSymbolRegistry(registry) {
|
|
560
|
+
this.symbolRegistry = registry;
|
|
561
|
+
}
|
|
562
|
+
loadProjectSources(files) {
|
|
563
|
+
for (const [filePath, source] of files) {
|
|
564
|
+
try {
|
|
565
|
+
const existing = this.project.getSourceFile(filePath);
|
|
566
|
+
if (existing) this.project.removeSourceFile(existing);
|
|
567
|
+
this.project.createSourceFile(filePath, source, { overwrite: true });
|
|
568
|
+
} catch (err) {
|
|
569
|
+
console.error(`[ctxo:ts-morph] Failed to preload ${filePath}: ${err.message}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
this.projectPreloaded = true;
|
|
573
|
+
}
|
|
574
|
+
clearProjectSources() {
|
|
575
|
+
for (const sf of this.project.getSourceFiles()) {
|
|
576
|
+
this.project.removeSourceFile(sf);
|
|
577
|
+
}
|
|
578
|
+
this.projectPreloaded = false;
|
|
579
|
+
}
|
|
53
580
|
extractSymbols(filePath, source) {
|
|
54
581
|
const sourceFile = this.parseSource(filePath, source);
|
|
55
582
|
if (!sourceFile) return [];
|
|
@@ -76,12 +603,15 @@ var TsMorphAdapter = class {
|
|
|
76
603
|
this.extractImportEdges(sourceFile, filePath, edges);
|
|
77
604
|
this.extractInheritanceEdges(sourceFile, filePath, edges);
|
|
78
605
|
this.extractCallEdges(sourceFile, filePath, edges);
|
|
606
|
+
this.extractReferenceEdges(sourceFile, filePath, edges);
|
|
79
607
|
return edges;
|
|
80
608
|
} catch (err) {
|
|
81
609
|
console.error(`[ctxo:ts-morph] Edge extraction failed for ${filePath}: ${err.message}`);
|
|
82
610
|
return [];
|
|
83
611
|
} finally {
|
|
84
|
-
this.
|
|
612
|
+
if (!this.projectPreloaded) {
|
|
613
|
+
this.cleanupSourceFile(filePath);
|
|
614
|
+
}
|
|
85
615
|
}
|
|
86
616
|
}
|
|
87
617
|
extractComplexity(filePath, source) {
|
|
@@ -124,7 +654,9 @@ var TsMorphAdapter = class {
|
|
|
124
654
|
name,
|
|
125
655
|
kind: "function",
|
|
126
656
|
startLine: fn.getStartLineNumber() - 1,
|
|
127
|
-
endLine: fn.getEndLineNumber() - 1
|
|
657
|
+
endLine: fn.getEndLineNumber() - 1,
|
|
658
|
+
startOffset: fn.getStart(),
|
|
659
|
+
endOffset: fn.getEnd()
|
|
128
660
|
});
|
|
129
661
|
}
|
|
130
662
|
}
|
|
@@ -137,7 +669,9 @@ var TsMorphAdapter = class {
|
|
|
137
669
|
name,
|
|
138
670
|
kind: "class",
|
|
139
671
|
startLine: cls.getStartLineNumber() - 1,
|
|
140
|
-
endLine: cls.getEndLineNumber() - 1
|
|
672
|
+
endLine: cls.getEndLineNumber() - 1,
|
|
673
|
+
startOffset: cls.getStart(),
|
|
674
|
+
endOffset: cls.getEnd()
|
|
141
675
|
});
|
|
142
676
|
for (const method of cls.getMethods()) {
|
|
143
677
|
const methodName = method.getName();
|
|
@@ -146,7 +680,9 @@ var TsMorphAdapter = class {
|
|
|
146
680
|
name: `${name}.${methodName}`,
|
|
147
681
|
kind: "method",
|
|
148
682
|
startLine: method.getStartLineNumber() - 1,
|
|
149
|
-
endLine: method.getEndLineNumber() - 1
|
|
683
|
+
endLine: method.getEndLineNumber() - 1,
|
|
684
|
+
startOffset: method.getStart(),
|
|
685
|
+
endOffset: method.getEnd()
|
|
150
686
|
});
|
|
151
687
|
}
|
|
152
688
|
}
|
|
@@ -160,7 +696,9 @@ var TsMorphAdapter = class {
|
|
|
160
696
|
name,
|
|
161
697
|
kind: "interface",
|
|
162
698
|
startLine: iface.getStartLineNumber() - 1,
|
|
163
|
-
endLine: iface.getEndLineNumber() - 1
|
|
699
|
+
endLine: iface.getEndLineNumber() - 1,
|
|
700
|
+
startOffset: iface.getStart(),
|
|
701
|
+
endOffset: iface.getEnd()
|
|
164
702
|
});
|
|
165
703
|
}
|
|
166
704
|
}
|
|
@@ -173,7 +711,9 @@ var TsMorphAdapter = class {
|
|
|
173
711
|
name,
|
|
174
712
|
kind: "type",
|
|
175
713
|
startLine: typeAlias.getStartLineNumber() - 1,
|
|
176
|
-
endLine: typeAlias.getEndLineNumber() - 1
|
|
714
|
+
endLine: typeAlias.getEndLineNumber() - 1,
|
|
715
|
+
startOffset: typeAlias.getStart(),
|
|
716
|
+
endOffset: typeAlias.getEnd()
|
|
177
717
|
});
|
|
178
718
|
}
|
|
179
719
|
}
|
|
@@ -187,7 +727,9 @@ var TsMorphAdapter = class {
|
|
|
187
727
|
name,
|
|
188
728
|
kind: "variable",
|
|
189
729
|
startLine: stmt.getStartLineNumber() - 1,
|
|
190
|
-
endLine: stmt.getEndLineNumber() - 1
|
|
730
|
+
endLine: stmt.getEndLineNumber() - 1,
|
|
731
|
+
startOffset: decl.getStart(),
|
|
732
|
+
endOffset: decl.getEnd()
|
|
191
733
|
});
|
|
192
734
|
}
|
|
193
735
|
}
|
|
@@ -195,7 +737,7 @@ var TsMorphAdapter = class {
|
|
|
195
737
|
// ── Edge Extraction ─────────────────────────────────────────
|
|
196
738
|
extractImportEdges(sourceFile, filePath, edges) {
|
|
197
739
|
const fileSymbolId = this.buildSymbolId(filePath, sourceFile.getBaseName().replace(/\.[^.]+$/, ""), "variable");
|
|
198
|
-
const fromSymbols = this.
|
|
740
|
+
const fromSymbols = this.getExportedSymbolIds(sourceFile, filePath);
|
|
199
741
|
const fromSymbol = fromSymbols.length > 0 ? fromSymbols[0] : fileSymbolId;
|
|
200
742
|
for (const imp of sourceFile.getImportDeclarations()) {
|
|
201
743
|
const moduleSpecifier = imp.getModuleSpecifierValue();
|
|
@@ -203,21 +745,36 @@ var TsMorphAdapter = class {
|
|
|
203
745
|
continue;
|
|
204
746
|
}
|
|
205
747
|
const normalizedTarget = this.resolveRelativeImport(filePath, moduleSpecifier);
|
|
748
|
+
const isTypeOnly = imp.isTypeOnly();
|
|
206
749
|
for (const named of imp.getNamedImports()) {
|
|
207
750
|
const importedName = named.getName();
|
|
208
|
-
|
|
751
|
+
const edge = {
|
|
209
752
|
from: fromSymbol,
|
|
210
753
|
to: this.resolveImportTarget(normalizedTarget, importedName),
|
|
211
754
|
kind: "imports"
|
|
212
|
-
}
|
|
755
|
+
};
|
|
756
|
+
if (isTypeOnly || named.isTypeOnly()) edge.typeOnly = true;
|
|
757
|
+
edges.push(edge);
|
|
213
758
|
}
|
|
214
759
|
const defaultImport = imp.getDefaultImport();
|
|
215
760
|
if (defaultImport) {
|
|
216
|
-
|
|
761
|
+
const edge = {
|
|
217
762
|
from: fromSymbol,
|
|
218
763
|
to: this.resolveImportTarget(normalizedTarget, defaultImport.getText()),
|
|
219
764
|
kind: "imports"
|
|
220
|
-
}
|
|
765
|
+
};
|
|
766
|
+
if (isTypeOnly) edge.typeOnly = true;
|
|
767
|
+
edges.push(edge);
|
|
768
|
+
}
|
|
769
|
+
const nsImport = imp.getNamespaceImport();
|
|
770
|
+
if (nsImport) {
|
|
771
|
+
const edge = {
|
|
772
|
+
from: fromSymbol,
|
|
773
|
+
to: this.buildSymbolId(normalizedTarget, nsImport.getText(), "variable"),
|
|
774
|
+
kind: "imports"
|
|
775
|
+
};
|
|
776
|
+
if (isTypeOnly) edge.typeOnly = true;
|
|
777
|
+
edges.push(edge);
|
|
221
778
|
}
|
|
222
779
|
}
|
|
223
780
|
}
|
|
@@ -228,7 +785,7 @@ var TsMorphAdapter = class {
|
|
|
228
785
|
const classSymbolId = this.buildSymbolId(filePath, className, "class");
|
|
229
786
|
const baseClass = cls.getExtends();
|
|
230
787
|
if (baseClass) {
|
|
231
|
-
const baseName = baseClass.getExpression().getText();
|
|
788
|
+
const baseName = baseClass.getExpression().getText().replace(/<.*>$/, "");
|
|
232
789
|
edges.push({
|
|
233
790
|
from: classSymbolId,
|
|
234
791
|
to: this.resolveSymbolReference(sourceFile, baseName, "class"),
|
|
@@ -236,7 +793,7 @@ var TsMorphAdapter = class {
|
|
|
236
793
|
});
|
|
237
794
|
}
|
|
238
795
|
for (const impl of cls.getImplements()) {
|
|
239
|
-
const ifaceName = impl.getExpression().getText();
|
|
796
|
+
const ifaceName = impl.getExpression().getText().replace(/<.*>$/, "");
|
|
240
797
|
edges.push({
|
|
241
798
|
from: classSymbolId,
|
|
242
799
|
to: this.resolveSymbolReference(sourceFile, ifaceName, "interface"),
|
|
@@ -259,6 +816,14 @@ var TsMorphAdapter = class {
|
|
|
259
816
|
edges.push({ from: fnSymbolId, to: resolved, kind: "calls" });
|
|
260
817
|
}
|
|
261
818
|
}
|
|
819
|
+
for (const newExpr of fn.getDescendantsOfKind(SyntaxKind.NewExpression)) {
|
|
820
|
+
const calledName = newExpr.getExpression().getText().split(".").pop();
|
|
821
|
+
if (!calledName) continue;
|
|
822
|
+
const resolved = this.resolveLocalCallTarget(sourceFile, filePath, calledName);
|
|
823
|
+
if (resolved) {
|
|
824
|
+
edges.push({ from: fnSymbolId, to: resolved, kind: "calls" });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
262
827
|
}
|
|
263
828
|
for (const cls of sourceFile.getClasses()) {
|
|
264
829
|
const className = cls.getName();
|
|
@@ -270,8 +835,26 @@ var TsMorphAdapter = class {
|
|
|
270
835
|
const calledText = call.getExpression().getText();
|
|
271
836
|
const calledName = calledText.split(".").pop();
|
|
272
837
|
if (!calledName || calledName === methodName) continue;
|
|
273
|
-
|
|
274
|
-
|
|
838
|
+
let resolved;
|
|
839
|
+
if (calledText.startsWith("this.")) {
|
|
840
|
+
resolved = this.resolveThisMethodCall(sourceFile, filePath, className, calledName);
|
|
841
|
+
} else {
|
|
842
|
+
resolved = this.resolveLocalCallTarget(sourceFile, filePath, calledName);
|
|
843
|
+
}
|
|
844
|
+
if (resolved) {
|
|
845
|
+
edges.push({ from: methodSymbolId, to: resolved, kind: "calls" });
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
for (const newExpr of method.getDescendantsOfKind(SyntaxKind.NewExpression)) {
|
|
849
|
+
const calledText = newExpr.getExpression().getText();
|
|
850
|
+
const calledName = calledText.split(".").pop();
|
|
851
|
+
if (!calledName) continue;
|
|
852
|
+
let resolved;
|
|
853
|
+
if (calledText.startsWith("this.")) {
|
|
854
|
+
resolved = this.resolveThisMethodCall(sourceFile, filePath, className, calledName);
|
|
855
|
+
} else {
|
|
856
|
+
resolved = this.resolveLocalCallTarget(sourceFile, filePath, calledName);
|
|
857
|
+
}
|
|
275
858
|
if (resolved) {
|
|
276
859
|
edges.push({ from: methodSymbolId, to: resolved, kind: "calls" });
|
|
277
860
|
}
|
|
@@ -279,6 +862,48 @@ var TsMorphAdapter = class {
|
|
|
279
862
|
}
|
|
280
863
|
}
|
|
281
864
|
}
|
|
865
|
+
extractReferenceEdges(sourceFile, filePath, edges) {
|
|
866
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
867
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
868
|
+
const mod = imp.getModuleSpecifierValue();
|
|
869
|
+
if (!mod.startsWith(".") && !mod.startsWith("/")) continue;
|
|
870
|
+
const targetFile = this.resolveRelativeImport(filePath, mod);
|
|
871
|
+
for (const named of imp.getNamedImports()) {
|
|
872
|
+
importMap.set(named.getName(), this.resolveImportTarget(targetFile, named.getName()));
|
|
873
|
+
}
|
|
874
|
+
const defaultImport = imp.getDefaultImport();
|
|
875
|
+
if (defaultImport) {
|
|
876
|
+
importMap.set(defaultImport.getText(), this.resolveImportTarget(targetFile, defaultImport.getText()));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (importMap.size === 0) return;
|
|
880
|
+
for (const fn of sourceFile.getFunctions()) {
|
|
881
|
+
if (!this.isExported(fn)) continue;
|
|
882
|
+
const fnName = fn.getName();
|
|
883
|
+
if (!fnName) continue;
|
|
884
|
+
const fromId = this.buildSymbolId(filePath, fnName, "function");
|
|
885
|
+
this.emitUsesEdges(fn, fromId, importMap, edges);
|
|
886
|
+
}
|
|
887
|
+
for (const cls of sourceFile.getClasses()) {
|
|
888
|
+
if (!this.isExported(cls)) continue;
|
|
889
|
+
const className = cls.getName();
|
|
890
|
+
if (!className) continue;
|
|
891
|
+
const classId = this.buildSymbolId(filePath, className, "class");
|
|
892
|
+
this.emitUsesEdges(cls, classId, importMap, edges);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
emitUsesEdges(node, fromId, importMap, edges) {
|
|
896
|
+
const seen = /* @__PURE__ */ new Set();
|
|
897
|
+
for (const id of node.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
898
|
+
const name = id.getText();
|
|
899
|
+
if (seen.has(name)) continue;
|
|
900
|
+
const target = importMap.get(name);
|
|
901
|
+
if (target) {
|
|
902
|
+
seen.add(name);
|
|
903
|
+
edges.push({ from: fromId, to: target, kind: "uses" });
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
282
907
|
resolveLocalCallTarget(sourceFile, filePath, calledName) {
|
|
283
908
|
for (const imp of sourceFile.getImportDeclarations()) {
|
|
284
909
|
const moduleSpecifier = imp.getModuleSpecifierValue();
|
|
@@ -297,10 +922,24 @@ var TsMorphAdapter = class {
|
|
|
297
922
|
}
|
|
298
923
|
return void 0;
|
|
299
924
|
}
|
|
925
|
+
resolveThisMethodCall(sourceFile, filePath, className, calledName) {
|
|
926
|
+
for (const cls of sourceFile.getClasses()) {
|
|
927
|
+
if (cls.getName() !== className) continue;
|
|
928
|
+
for (const method of cls.getMethods()) {
|
|
929
|
+
if (method.getName() === calledName) {
|
|
930
|
+
return this.buildSymbolId(filePath, `${className}.${calledName}`, "method");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return void 0;
|
|
935
|
+
}
|
|
300
936
|
// ── Helpers ─────────────────────────────────────────────────
|
|
301
937
|
parseSource(filePath, source) {
|
|
302
938
|
try {
|
|
303
939
|
const existing = this.project.getSourceFile(filePath);
|
|
940
|
+
if (existing && this.projectPreloaded) {
|
|
941
|
+
return existing;
|
|
942
|
+
}
|
|
304
943
|
if (existing) {
|
|
305
944
|
this.project.removeSourceFile(existing);
|
|
306
945
|
}
|
|
@@ -332,6 +971,12 @@ var TsMorphAdapter = class {
|
|
|
332
971
|
return resolved;
|
|
333
972
|
}
|
|
334
973
|
resolveImportTarget(targetFile, name) {
|
|
974
|
+
const prefix = `${targetFile}::${name}::`;
|
|
975
|
+
for (const kind2 of SYMBOL_KINDS) {
|
|
976
|
+
if (this.symbolRegistry.has(`${prefix}${kind2}`)) {
|
|
977
|
+
return `${prefix}${kind2}`;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
335
980
|
const targetSourceFile = this.project.getSourceFile(targetFile);
|
|
336
981
|
if (targetSourceFile) {
|
|
337
982
|
for (const fn of targetSourceFile.getFunctions()) {
|
|
@@ -364,8 +1009,13 @@ var TsMorphAdapter = class {
|
|
|
364
1009
|
if (!moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) continue;
|
|
365
1010
|
for (const named of imp.getNamedImports()) {
|
|
366
1011
|
if (named.getName() === name) {
|
|
367
|
-
const
|
|
368
|
-
|
|
1012
|
+
const resolved = imp.getModuleSpecifierSourceFile()?.getFilePath();
|
|
1013
|
+
if (resolved) {
|
|
1014
|
+
return `${this.normalizeFilePath(resolved)}::${name}::${defaultKind}`;
|
|
1015
|
+
}
|
|
1016
|
+
const sourceDir = dirname(this.normalizeFilePath(sourceFile.getFilePath()));
|
|
1017
|
+
const resolvedPath = normalize(join(sourceDir, moduleSpecifier)).replace(/\\/g, "/").replace(/\.jsx$/, ".tsx").replace(/\.js$/, ".ts");
|
|
1018
|
+
return `${resolvedPath}::${name}::${defaultKind}`;
|
|
369
1019
|
}
|
|
370
1020
|
}
|
|
371
1021
|
}
|
|
@@ -380,9 +1030,7 @@ var TsMorphAdapter = class {
|
|
|
380
1030
|
normalizeFilePath(filePath) {
|
|
381
1031
|
return filePath.replace(/^\//, "");
|
|
382
1032
|
}
|
|
383
|
-
|
|
384
|
-
const sourceFile = this.project.getSourceFile(filePath);
|
|
385
|
-
if (!sourceFile) return [];
|
|
1033
|
+
getExportedSymbolIds(sourceFile, filePath) {
|
|
386
1034
|
const ids = [];
|
|
387
1035
|
for (const fn of sourceFile.getFunctions()) {
|
|
388
1036
|
if (!this.isExported(fn)) continue;
|
|
@@ -455,6 +1103,9 @@ var LanguageAdapterRegistry = class {
|
|
|
455
1103
|
this.adaptersByExtension.set(ext.toLowerCase(), adapter);
|
|
456
1104
|
}
|
|
457
1105
|
}
|
|
1106
|
+
getSupportedExtensions() {
|
|
1107
|
+
return new Set(this.adaptersByExtension.keys());
|
|
1108
|
+
}
|
|
458
1109
|
getAdapter(filePath) {
|
|
459
1110
|
if (!filePath) return void 0;
|
|
460
1111
|
const ext = extname2(filePath).toLowerCase();
|
|
@@ -464,7 +1115,7 @@ var LanguageAdapterRegistry = class {
|
|
|
464
1115
|
};
|
|
465
1116
|
|
|
466
1117
|
// src/adapters/storage/json-index-writer.ts
|
|
467
|
-
import { writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
|
|
1118
|
+
import { writeFileSync, renameSync, mkdirSync, unlinkSync, existsSync } from "fs";
|
|
468
1119
|
import { dirname as dirname2, join as join2, resolve, sep } from "path";
|
|
469
1120
|
var JsonIndexWriter = class {
|
|
470
1121
|
indexDir;
|
|
@@ -479,7 +1130,17 @@ var JsonIndexWriter = class {
|
|
|
479
1130
|
mkdirSync(dirname2(targetPath), { recursive: true });
|
|
480
1131
|
const sorted = this.sortKeys(fileIndex);
|
|
481
1132
|
const json = JSON.stringify(sorted, null, 2);
|
|
482
|
-
|
|
1133
|
+
this.atomicWrite(targetPath, json);
|
|
1134
|
+
}
|
|
1135
|
+
writeCoChanges(matrix) {
|
|
1136
|
+
mkdirSync(this.indexDir, { recursive: true });
|
|
1137
|
+
const targetPath = join2(this.indexDir, "co-changes.json");
|
|
1138
|
+
this.atomicWrite(targetPath, JSON.stringify(matrix, null, 2));
|
|
1139
|
+
}
|
|
1140
|
+
atomicWrite(targetPath, content) {
|
|
1141
|
+
const tmpPath = `${targetPath}.${process.pid}.tmp`;
|
|
1142
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
1143
|
+
renameSync(tmpPath, targetPath);
|
|
483
1144
|
}
|
|
484
1145
|
delete(relativePath) {
|
|
485
1146
|
const targetPath = this.resolveIndexPath(relativePath);
|
|
@@ -543,16 +1204,21 @@ var SchemaManager = class {
|
|
|
543
1204
|
var IndexCommand = class {
|
|
544
1205
|
projectRoot;
|
|
545
1206
|
ctxoRoot;
|
|
1207
|
+
supportedExtensions;
|
|
546
1208
|
constructor(projectRoot, ctxoRoot) {
|
|
547
1209
|
this.projectRoot = projectRoot;
|
|
548
1210
|
this.ctxoRoot = ctxoRoot ?? join4(projectRoot, ".ctxo");
|
|
1211
|
+
this.supportedExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".go", ".cs"]);
|
|
549
1212
|
}
|
|
550
1213
|
async run(options = {}) {
|
|
551
1214
|
if (options.check) {
|
|
552
1215
|
return this.runCheck();
|
|
553
1216
|
}
|
|
554
1217
|
const registry = new LanguageAdapterRegistry();
|
|
555
|
-
|
|
1218
|
+
const tsMorphAdapter = new TsMorphAdapter();
|
|
1219
|
+
registry.register(tsMorphAdapter);
|
|
1220
|
+
this.registerTreeSitterAdapters(registry);
|
|
1221
|
+
this.supportedExtensions = registry.getSupportedExtensions();
|
|
556
1222
|
const writer = new JsonIndexWriter(this.ctxoRoot);
|
|
557
1223
|
const schemaManager = new SchemaManager(this.ctxoRoot);
|
|
558
1224
|
const hasher = new ContentHasher();
|
|
@@ -572,7 +1238,8 @@ var IndexCommand = class {
|
|
|
572
1238
|
}
|
|
573
1239
|
console.error(`[ctxo] Building codebase index... Found ${files.length} source files`);
|
|
574
1240
|
}
|
|
575
|
-
const
|
|
1241
|
+
const symbolRegistry = /* @__PURE__ */ new Map();
|
|
1242
|
+
const pendingIndices = [];
|
|
576
1243
|
let processed = 0;
|
|
577
1244
|
for (const filePath of files) {
|
|
578
1245
|
const adapter = registry.getAdapter(filePath);
|
|
@@ -582,40 +1249,73 @@ var IndexCommand = class {
|
|
|
582
1249
|
const source = readFileSync2(filePath, "utf-8");
|
|
583
1250
|
const lastModified = Math.floor(Date.now() / 1e3);
|
|
584
1251
|
const symbols = adapter.extractSymbols(relativePath, source);
|
|
585
|
-
const edges = adapter.extractEdges(relativePath, source);
|
|
586
1252
|
const complexity = adapter.extractComplexity(relativePath, source);
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (!options.skipHistory) {
|
|
590
|
-
const commits = await gitAdapter.getCommitHistory(relativePath);
|
|
591
|
-
intent = commits.map((c) => ({
|
|
592
|
-
hash: c.hash,
|
|
593
|
-
message: c.message,
|
|
594
|
-
date: c.date,
|
|
595
|
-
kind: "commit"
|
|
596
|
-
}));
|
|
597
|
-
antiPatterns = revertDetector.detect(commits);
|
|
1253
|
+
for (const sym of symbols) {
|
|
1254
|
+
symbolRegistry.set(sym.symbolId, sym.kind);
|
|
598
1255
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1256
|
+
pendingIndices.push({
|
|
1257
|
+
relativePath,
|
|
1258
|
+
source,
|
|
1259
|
+
fileIndex: {
|
|
1260
|
+
file: relativePath,
|
|
1261
|
+
lastModified,
|
|
1262
|
+
contentHash: hasher.hash(source),
|
|
1263
|
+
symbols,
|
|
1264
|
+
edges: [],
|
|
1265
|
+
complexity,
|
|
1266
|
+
intent: [],
|
|
1267
|
+
antiPatterns: []
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
611
1270
|
processed++;
|
|
612
1271
|
if (processed % 50 === 0) {
|
|
613
|
-
console.error(`[ctxo] Processed ${processed}/${files.length} files`);
|
|
1272
|
+
console.error(`[ctxo] Processed ${processed}/${files.length} files (symbols)`);
|
|
614
1273
|
}
|
|
615
1274
|
} catch (err) {
|
|
616
1275
|
console.error(`[ctxo] Skipped ${relativePath}: ${err.message}`);
|
|
617
1276
|
}
|
|
618
1277
|
}
|
|
1278
|
+
const allSources = /* @__PURE__ */ new Map();
|
|
1279
|
+
for (const entry of pendingIndices) {
|
|
1280
|
+
allSources.set(entry.relativePath, entry.source);
|
|
1281
|
+
}
|
|
1282
|
+
tsMorphAdapter.loadProjectSources(allSources);
|
|
1283
|
+
for (const entry of pendingIndices) {
|
|
1284
|
+
const adapter = registry.getAdapter(entry.relativePath);
|
|
1285
|
+
if (!adapter) continue;
|
|
1286
|
+
try {
|
|
1287
|
+
adapter.setSymbolRegistry?.(symbolRegistry);
|
|
1288
|
+
entry.fileIndex.edges = adapter.extractEdges(entry.relativePath, entry.source);
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
console.error(`[ctxo] Edge extraction failed for ${entry.relativePath}: ${err.message}`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
tsMorphAdapter.clearProjectSources();
|
|
1294
|
+
if (!options.skipHistory && pendingIndices.length > 0) {
|
|
1295
|
+
const maxHistory = options.maxHistory ?? 20;
|
|
1296
|
+
const batchHistory = await gitAdapter.getBatchHistory?.(maxHistory) ?? /* @__PURE__ */ new Map();
|
|
1297
|
+
for (const { relativePath, fileIndex } of pendingIndices) {
|
|
1298
|
+
const commits = batchHistory.get(relativePath) ?? [];
|
|
1299
|
+
fileIndex.intent = commits.map((c) => ({
|
|
1300
|
+
hash: c.hash,
|
|
1301
|
+
message: c.message,
|
|
1302
|
+
date: c.date,
|
|
1303
|
+
kind: "commit"
|
|
1304
|
+
}));
|
|
1305
|
+
fileIndex.antiPatterns = revertDetector.detect(commits);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (!options.skipHistory && pendingIndices.length > 0) {
|
|
1309
|
+
const fileIndices = pendingIndices.map((e) => e.fileIndex);
|
|
1310
|
+
const coChangeMatrix = aggregateCoChanges(fileIndices);
|
|
1311
|
+
writer.writeCoChanges(coChangeMatrix);
|
|
1312
|
+
console.error(`[ctxo] Co-change analysis: ${coChangeMatrix.entries.length} file pairs detected`);
|
|
1313
|
+
}
|
|
1314
|
+
const indices = [];
|
|
1315
|
+
for (const { fileIndex } of pendingIndices) {
|
|
1316
|
+
writer.write(fileIndex);
|
|
1317
|
+
indices.push(fileIndex);
|
|
1318
|
+
}
|
|
619
1319
|
schemaManager.writeVersion();
|
|
620
1320
|
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
621
1321
|
try {
|
|
@@ -629,6 +1329,20 @@ var IndexCommand = class {
|
|
|
629
1329
|
}
|
|
630
1330
|
console.error(`[ctxo] Index complete: ${processed} files indexed`);
|
|
631
1331
|
}
|
|
1332
|
+
registerTreeSitterAdapters(registry) {
|
|
1333
|
+
try {
|
|
1334
|
+
const { GoAdapter: GoAdapter2 } = (init_go_adapter(), __toCommonJS(go_adapter_exports));
|
|
1335
|
+
registry.register(new GoAdapter2());
|
|
1336
|
+
} catch {
|
|
1337
|
+
console.error("[ctxo] Go adapter unavailable (tree-sitter-go not installed)");
|
|
1338
|
+
}
|
|
1339
|
+
try {
|
|
1340
|
+
const { CSharpAdapter: CSharpAdapter2 } = (init_csharp_adapter(), __toCommonJS(csharp_adapter_exports));
|
|
1341
|
+
registry.register(new CSharpAdapter2());
|
|
1342
|
+
} catch {
|
|
1343
|
+
console.error("[ctxo] C# adapter unavailable (tree-sitter-c-sharp not installed)");
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
632
1346
|
discoverFilesIn(root) {
|
|
633
1347
|
try {
|
|
634
1348
|
const output = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
@@ -675,20 +1389,23 @@ var IndexCommand = class {
|
|
|
675
1389
|
}
|
|
676
1390
|
}
|
|
677
1391
|
isSupportedExtension(filePath) {
|
|
678
|
-
const ext =
|
|
679
|
-
return
|
|
1392
|
+
const ext = extname4(filePath).toLowerCase();
|
|
1393
|
+
return this.supportedExtensions.has(ext);
|
|
680
1394
|
}
|
|
681
1395
|
async runCheck() {
|
|
682
1396
|
console.error("[ctxo] Checking index freshness...");
|
|
1397
|
+
const registry = new LanguageAdapterRegistry();
|
|
1398
|
+
registry.register(new TsMorphAdapter());
|
|
1399
|
+
this.registerTreeSitterAdapters(registry);
|
|
1400
|
+
this.supportedExtensions = registry.getSupportedExtensions();
|
|
683
1401
|
const hasher = new ContentHasher();
|
|
684
1402
|
const files = this.discoverFilesIn(this.projectRoot);
|
|
685
|
-
const reader = new (await import("./json-index-reader-
|
|
1403
|
+
const reader = new (await import("./json-index-reader-FCKSKA6R.js")).JsonIndexReader(this.ctxoRoot);
|
|
686
1404
|
const indices = reader.readAll();
|
|
687
1405
|
const indexedMap = new Map(indices.map((i) => [i.file, i]));
|
|
688
1406
|
let staleCount = 0;
|
|
689
1407
|
for (const filePath of files) {
|
|
690
|
-
|
|
691
|
-
if (![".ts", ".tsx", ".js", ".jsx"].includes(ext)) continue;
|
|
1408
|
+
if (!this.isSupportedExtension(filePath)) continue;
|
|
692
1409
|
const relativePath = relative(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
693
1410
|
const indexed = indexedMap.get(relativePath);
|
|
694
1411
|
if (!indexed) {
|
|
@@ -911,7 +1628,7 @@ var InitCommand = class {
|
|
|
911
1628
|
};
|
|
912
1629
|
|
|
913
1630
|
// src/cli/watch-command.ts
|
|
914
|
-
import { join as join9, extname as
|
|
1631
|
+
import { join as join9, extname as extname5, relative as relative2 } from "path";
|
|
915
1632
|
import { readFileSync as readFileSync4 } from "fs";
|
|
916
1633
|
|
|
917
1634
|
// src/adapters/watcher/chokidar-watcher-adapter.ts
|
|
@@ -953,7 +1670,6 @@ var ChokidarWatcherAdapter = class {
|
|
|
953
1670
|
};
|
|
954
1671
|
|
|
955
1672
|
// src/cli/watch-command.ts
|
|
956
|
-
var SUPPORTED_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
957
1673
|
var DEBOUNCE_MS = 300;
|
|
958
1674
|
var WatchCommand = class {
|
|
959
1675
|
projectRoot;
|
|
@@ -966,6 +1682,8 @@ var WatchCommand = class {
|
|
|
966
1682
|
console.error("[ctxo] Starting file watcher...");
|
|
967
1683
|
const registry = new LanguageAdapterRegistry();
|
|
968
1684
|
registry.register(new TsMorphAdapter());
|
|
1685
|
+
this.registerTreeSitterAdapters(registry);
|
|
1686
|
+
const supportedExtensions = registry.getSupportedExtensions();
|
|
969
1687
|
const writer = new JsonIndexWriter(this.ctxoRoot);
|
|
970
1688
|
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
971
1689
|
await storage.init();
|
|
@@ -975,8 +1693,6 @@ var WatchCommand = class {
|
|
|
975
1693
|
const watcher = new ChokidarWatcherAdapter(this.projectRoot);
|
|
976
1694
|
const pendingFiles = /* @__PURE__ */ new Map();
|
|
977
1695
|
const reindexFile = async (filePath) => {
|
|
978
|
-
const ext = extname4(filePath).toLowerCase();
|
|
979
|
-
if (!SUPPORTED_EXTENSIONS2.has(ext)) return;
|
|
980
1696
|
const adapter = registry.getAdapter(filePath);
|
|
981
1697
|
if (!adapter) return;
|
|
982
1698
|
const relativePath = relative2(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
@@ -1024,8 +1740,8 @@ var WatchCommand = class {
|
|
|
1024
1740
|
};
|
|
1025
1741
|
watcher.start((event, filePath) => {
|
|
1026
1742
|
if (event === "unlink") {
|
|
1027
|
-
const ext =
|
|
1028
|
-
if (!
|
|
1743
|
+
const ext = extname5(filePath).toLowerCase();
|
|
1744
|
+
if (!supportedExtensions.has(ext)) return;
|
|
1029
1745
|
const relativePath = relative2(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
1030
1746
|
writer.delete(relativePath);
|
|
1031
1747
|
storage.deleteSymbolFile(relativePath);
|
|
@@ -1049,6 +1765,20 @@ var WatchCommand = class {
|
|
|
1049
1765
|
process.on("SIGINT", cleanup);
|
|
1050
1766
|
process.on("SIGTERM", cleanup);
|
|
1051
1767
|
}
|
|
1768
|
+
registerTreeSitterAdapters(registry) {
|
|
1769
|
+
try {
|
|
1770
|
+
const { GoAdapter: GoAdapter2 } = (init_go_adapter(), __toCommonJS(go_adapter_exports));
|
|
1771
|
+
registry.register(new GoAdapter2());
|
|
1772
|
+
} catch {
|
|
1773
|
+
console.error("[ctxo] Go adapter unavailable (tree-sitter-go not installed)");
|
|
1774
|
+
}
|
|
1775
|
+
try {
|
|
1776
|
+
const { CSharpAdapter: CSharpAdapter2 } = (init_csharp_adapter(), __toCommonJS(csharp_adapter_exports));
|
|
1777
|
+
registry.register(new CSharpAdapter2());
|
|
1778
|
+
} catch {
|
|
1779
|
+
console.error("[ctxo] C# adapter unavailable (tree-sitter-c-sharp not installed)");
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1052
1782
|
};
|
|
1053
1783
|
|
|
1054
1784
|
// src/cli/cli-router.ts
|
|
@@ -1070,10 +1800,18 @@ var CliRouter = class {
|
|
|
1070
1800
|
if (fileIdx !== -1 && (!fileArg || fileArg.startsWith("--"))) {
|
|
1071
1801
|
console.error("[ctxo] --file requires a path argument");
|
|
1072
1802
|
process.exit(1);
|
|
1803
|
+
return;
|
|
1073
1804
|
}
|
|
1074
1805
|
const checkArg = args.includes("--check");
|
|
1075
1806
|
const skipHistory = args.includes("--skip-history");
|
|
1076
|
-
|
|
1807
|
+
const maxHistoryIdx = args.indexOf("--max-history");
|
|
1808
|
+
const maxHistoryArg = maxHistoryIdx !== -1 ? Number(args[maxHistoryIdx + 1]) : void 0;
|
|
1809
|
+
if (maxHistoryIdx !== -1 && (!maxHistoryArg || isNaN(maxHistoryArg) || maxHistoryArg < 1)) {
|
|
1810
|
+
console.error("[ctxo] --max-history requires a positive integer");
|
|
1811
|
+
process.exit(1);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
await new IndexCommand(this.projectRoot).run({ file: fileArg, check: checkArg, skipHistory, maxHistory: maxHistoryArg });
|
|
1077
1815
|
break;
|
|
1078
1816
|
}
|
|
1079
1817
|
case "sync":
|
|
@@ -1094,6 +1832,7 @@ var CliRouter = class {
|
|
|
1094
1832
|
default:
|
|
1095
1833
|
console.error(`[ctxo] Unknown command: "${command}". Run "ctxo --help" for usage.`);
|
|
1096
1834
|
process.exit(1);
|
|
1835
|
+
return;
|
|
1097
1836
|
}
|
|
1098
1837
|
}
|
|
1099
1838
|
printHelp() {
|
|
@@ -1102,7 +1841,7 @@ ctxo \u2014 MCP server for dependency-aware codebase context
|
|
|
1102
1841
|
|
|
1103
1842
|
Usage:
|
|
1104
1843
|
ctxo Start MCP server (stdio transport)
|
|
1105
|
-
ctxo index Build full codebase index
|
|
1844
|
+
ctxo index Build full codebase index (--max-history N, default 20)
|
|
1106
1845
|
ctxo sync Rebuild SQLite cache from committed JSON index
|
|
1107
1846
|
ctxo watch Start file watcher for incremental re-indexing
|
|
1108
1847
|
ctxo verify-index CI gate: fail if index is stale
|
|
@@ -1115,4 +1854,4 @@ Usage:
|
|
|
1115
1854
|
export {
|
|
1116
1855
|
CliRouter
|
|
1117
1856
|
};
|
|
1118
|
-
//# sourceMappingURL=cli-router-
|
|
1857
|
+
//# sourceMappingURL=cli-router-NRUGPICL.js.map
|