@monoes/graph 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.
Files changed (82) hide show
  1. package/dist/src/analyze.d.ts +23 -0
  2. package/dist/src/analyze.d.ts.map +1 -0
  3. package/dist/src/analyze.js +105 -0
  4. package/dist/src/analyze.js.map +1 -0
  5. package/dist/src/build.d.ts +8 -0
  6. package/dist/src/build.d.ts.map +1 -0
  7. package/dist/src/build.js +59 -0
  8. package/dist/src/build.js.map +1 -0
  9. package/dist/src/cache.d.ts +10 -0
  10. package/dist/src/cache.d.ts.map +1 -0
  11. package/dist/src/cache.js +34 -0
  12. package/dist/src/cache.js.map +1 -0
  13. package/dist/src/cluster.d.ts +8 -0
  14. package/dist/src/cluster.d.ts.map +1 -0
  15. package/dist/src/cluster.js +50 -0
  16. package/dist/src/cluster.js.map +1 -0
  17. package/dist/src/detect.d.ts +8 -0
  18. package/dist/src/detect.d.ts.map +1 -0
  19. package/dist/src/detect.js +108 -0
  20. package/dist/src/detect.js.map +1 -0
  21. package/dist/src/export.d.ts +21 -0
  22. package/dist/src/export.d.ts.map +1 -0
  23. package/dist/src/export.js +68 -0
  24. package/dist/src/export.js.map +1 -0
  25. package/dist/src/extract/index.d.ts +20 -0
  26. package/dist/src/extract/index.d.ts.map +1 -0
  27. package/dist/src/extract/index.js +158 -0
  28. package/dist/src/extract/index.js.map +1 -0
  29. package/dist/src/extract/languages/go.d.ts +3 -0
  30. package/dist/src/extract/languages/go.d.ts.map +1 -0
  31. package/dist/src/extract/languages/go.js +181 -0
  32. package/dist/src/extract/languages/go.js.map +1 -0
  33. package/dist/src/extract/languages/python.d.ts +3 -0
  34. package/dist/src/extract/languages/python.d.ts.map +1 -0
  35. package/dist/src/extract/languages/python.js +230 -0
  36. package/dist/src/extract/languages/python.js.map +1 -0
  37. package/dist/src/extract/languages/rust.d.ts +3 -0
  38. package/dist/src/extract/languages/rust.d.ts.map +1 -0
  39. package/dist/src/extract/languages/rust.js +195 -0
  40. package/dist/src/extract/languages/rust.js.map +1 -0
  41. package/dist/src/extract/languages/typescript.d.ts +3 -0
  42. package/dist/src/extract/languages/typescript.d.ts.map +1 -0
  43. package/dist/src/extract/languages/typescript.js +295 -0
  44. package/dist/src/extract/languages/typescript.js.map +1 -0
  45. package/dist/src/extract/tree-sitter-runner.d.ts +48 -0
  46. package/dist/src/extract/tree-sitter-runner.d.ts.map +1 -0
  47. package/dist/src/extract/tree-sitter-runner.js +128 -0
  48. package/dist/src/extract/tree-sitter-runner.js.map +1 -0
  49. package/dist/src/extract/types.d.ts +7 -0
  50. package/dist/src/extract/types.d.ts.map +1 -0
  51. package/dist/src/extract/types.js +2 -0
  52. package/dist/src/extract/types.js.map +1 -0
  53. package/dist/src/index.d.ts +11 -0
  54. package/dist/src/index.d.ts.map +1 -0
  55. package/dist/src/index.js +9 -0
  56. package/dist/src/index.js.map +1 -0
  57. package/dist/src/pipeline.d.ts +16 -0
  58. package/dist/src/pipeline.d.ts.map +1 -0
  59. package/dist/src/pipeline.js +143 -0
  60. package/dist/src/pipeline.js.map +1 -0
  61. package/dist/src/types.d.ts +99 -0
  62. package/dist/src/types.d.ts.map +1 -0
  63. package/dist/src/types.js +2 -0
  64. package/dist/src/types.js.map +1 -0
  65. package/dist/tsconfig.tsbuildinfo +1 -0
  66. package/package.json +44 -0
  67. package/src/analyze.ts +122 -0
  68. package/src/build.ts +62 -0
  69. package/src/cache.ts +38 -0
  70. package/src/cluster.ts +54 -0
  71. package/src/detect.ts +123 -0
  72. package/src/export.ts +78 -0
  73. package/src/extract/index.ts +190 -0
  74. package/src/extract/languages/go.ts +206 -0
  75. package/src/extract/languages/python.ts +270 -0
  76. package/src/extract/languages/rust.ts +230 -0
  77. package/src/extract/languages/typescript.ts +344 -0
  78. package/src/extract/tree-sitter-runner.ts +165 -0
  79. package/src/extract/types.ts +7 -0
  80. package/src/index.ts +10 -0
  81. package/src/pipeline.ts +166 -0
  82. package/src/types.ts +116 -0
@@ -0,0 +1,230 @@
1
+ import { basename } from 'path';
2
+ import type { GraphNode, GraphEdge, ExtractionResult } from '../../types.js';
3
+ import type { LanguageExtractor } from '../types.js';
4
+ import {
5
+ tryLoadParser,
6
+ walk,
7
+ type SyntaxNodeLike,
8
+ } from '../tree-sitter-runner.js';
9
+
10
+ // ---- helpers ----
11
+
12
+ function nodeName(node: SyntaxNodeLike): string {
13
+ const nameNode = node.childForFieldName('name');
14
+ return nameNode?.text ?? '';
15
+ }
16
+
17
+ function loc(node: SyntaxNodeLike): string {
18
+ return `L${node.startPosition.row + 1}`;
19
+ }
20
+
21
+ // ---- tree-sitter extraction ----
22
+
23
+ function extractWithTreeSitter(filePath: string, content: string): ExtractionResult {
24
+ const nodes: GraphNode[] = [];
25
+ const edges: GraphEdge[] = [];
26
+ const errors: string[] = [];
27
+
28
+ const parser = tryLoadParser('rust');
29
+ if (!parser) {
30
+ return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
31
+ }
32
+
33
+ let tree: { rootNode: SyntaxNodeLike };
34
+ try {
35
+ tree = parser.parse(content);
36
+ } catch (err) {
37
+ errors.push(`tree-sitter parse error in ${filePath}: ${String(err)}`);
38
+ return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
39
+ }
40
+
41
+ walk(tree.rootNode, (n) => {
42
+ // ---- functions ----
43
+ if (n.type === 'function_item') {
44
+ const name = nodeName(n);
45
+ if (name) {
46
+ nodes.push({
47
+ id: name,
48
+ label: name,
49
+ fileType: 'code',
50
+ sourceFile: filePath,
51
+ sourceLocation: loc(n),
52
+ nodeKind: 'function',
53
+ });
54
+ }
55
+ return;
56
+ }
57
+
58
+ // ---- structs ----
59
+ if (n.type === 'struct_item') {
60
+ const name = nodeName(n);
61
+ if (name) {
62
+ nodes.push({
63
+ id: name,
64
+ label: name,
65
+ fileType: 'code',
66
+ sourceFile: filePath,
67
+ sourceLocation: loc(n),
68
+ nodeKind: 'struct',
69
+ });
70
+ }
71
+ return;
72
+ }
73
+
74
+ // ---- traits ----
75
+ if (n.type === 'trait_item') {
76
+ const name = nodeName(n);
77
+ if (name) {
78
+ nodes.push({
79
+ id: name,
80
+ label: name,
81
+ fileType: 'code',
82
+ sourceFile: filePath,
83
+ sourceLocation: loc(n),
84
+ nodeKind: 'trait',
85
+ });
86
+ }
87
+ return;
88
+ }
89
+
90
+ // ---- impl blocks ----
91
+ if (n.type === 'impl_item') {
92
+ const typeNode = n.childForFieldName('type');
93
+ const traitNode = n.childForFieldName('trait');
94
+ const typeName = typeNode?.text ?? '';
95
+
96
+ if (typeName) {
97
+ nodes.push({
98
+ id: typeName,
99
+ label: typeName,
100
+ fileType: 'code',
101
+ sourceFile: filePath,
102
+ sourceLocation: loc(n),
103
+ nodeKind: 'impl',
104
+ });
105
+
106
+ // impl Trait for Type
107
+ if (traitNode) {
108
+ edges.push({
109
+ source: typeName,
110
+ target: traitNode.text,
111
+ relation: 'implements',
112
+ confidence: 'EXTRACTED',
113
+ sourceFile: filePath,
114
+ sourceLocation: loc(n),
115
+ });
116
+ }
117
+ }
118
+ return;
119
+ }
120
+
121
+ // ---- modules ----
122
+ if (n.type === 'mod_item') {
123
+ const name = nodeName(n);
124
+ if (name) {
125
+ nodes.push({
126
+ id: name,
127
+ label: name,
128
+ fileType: 'code',
129
+ sourceFile: filePath,
130
+ sourceLocation: loc(n),
131
+ nodeKind: 'module',
132
+ });
133
+ }
134
+ return;
135
+ }
136
+
137
+ // ---- use declarations ----
138
+ if (n.type === 'use_declaration') {
139
+ const argNode = n.childForFieldName('argument');
140
+ if (argNode) {
141
+ // Flatten the use path to a string (handles use a::b::c and use a::b::{c, d})
142
+ const usePath = argNode.text.replace(/\s+/g, '');
143
+ edges.push({
144
+ source: basename(filePath),
145
+ target: usePath,
146
+ relation: 'imports',
147
+ confidence: 'EXTRACTED',
148
+ sourceFile: filePath,
149
+ sourceLocation: loc(n),
150
+ });
151
+ }
152
+ }
153
+ });
154
+
155
+ return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
156
+ }
157
+
158
+ // ---- regex fallback ----
159
+
160
+ function extractWithRegex(filePath: string, content: string): ExtractionResult {
161
+ const nodes: GraphNode[] = [];
162
+ const edges: GraphEdge[] = [];
163
+
164
+ const lines = content.split('\n');
165
+
166
+ lines.forEach((line, idx) => {
167
+ const location = `L${idx + 1}`;
168
+ const trimmed = line.trim();
169
+
170
+ // pub fn / fn
171
+ const fnMatch = trimmed.match(/^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
172
+ if (fnMatch) {
173
+ nodes.push({ id: fnMatch[1], label: fnMatch[1], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'function' });
174
+ }
175
+
176
+ // pub struct / struct
177
+ const structMatch = trimmed.match(/^(?:pub\s+)?struct\s+(\w+)/);
178
+ if (structMatch) {
179
+ nodes.push({ id: structMatch[1], label: structMatch[1], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'struct' });
180
+ }
181
+
182
+ // pub trait / trait
183
+ const traitMatch = trimmed.match(/^(?:pub\s+)?trait\s+(\w+)/);
184
+ if (traitMatch) {
185
+ nodes.push({ id: traitMatch[1], label: traitMatch[1], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'trait' });
186
+ }
187
+
188
+ // impl Trait for Type
189
+ const implForMatch = trimmed.match(/^impl(?:<[^>]+>)?\s+(\w+)\s+for\s+(\w+)/);
190
+ if (implForMatch) {
191
+ edges.push({ source: implForMatch[2], target: implForMatch[1], relation: 'implements', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
192
+ nodes.push({ id: implForMatch[2], label: implForMatch[2], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'impl' });
193
+ }
194
+
195
+ // plain impl Type
196
+ const implMatch = trimmed.match(/^impl(?:<[^>]+>)?\s+(\w+)\s*\{/);
197
+ if (implMatch && !implForMatch) {
198
+ nodes.push({ id: implMatch[1], label: implMatch[1], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'impl' });
199
+ }
200
+
201
+ // use statements
202
+ const useMatch = trimmed.match(/^use\s+([^;]+)/);
203
+ if (useMatch) {
204
+ edges.push({ source: basename(filePath), target: useMatch[1].trim(), relation: 'imports', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
205
+ }
206
+
207
+ // mod declarations
208
+ const modMatch = trimmed.match(/^(?:pub\s+)?mod\s+(\w+)/);
209
+ if (modMatch) {
210
+ nodes.push({ id: modMatch[1], label: modMatch[1], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'module' });
211
+ }
212
+ });
213
+
214
+ return { nodes, edges, filesProcessed: 1, fromCache: 0, errors: [] };
215
+ }
216
+
217
+ // ---- extractor implementation ----
218
+
219
+ export const rustExtractor: LanguageExtractor = {
220
+ language: 'rust',
221
+ extensions: ['.rs'],
222
+
223
+ extract(filePath: string, content: string): ExtractionResult {
224
+ const tsResult = extractWithTreeSitter(filePath, content);
225
+ if (tsResult.nodes.length > 0 || tsResult.edges.length > 0 || tsResult.errors.length > 0) {
226
+ return tsResult;
227
+ }
228
+ return extractWithRegex(filePath, content);
229
+ },
230
+ };
@@ -0,0 +1,344 @@
1
+ import { basename } from 'path';
2
+ import type { GraphNode, GraphEdge, ExtractionResult } from '../../types.js';
3
+ import type { LanguageExtractor } from '../types.js';
4
+ import {
5
+ tryLoadParser,
6
+ walk,
7
+ type SyntaxNodeLike,
8
+ } from '../tree-sitter-runner.js';
9
+
10
+ // ---- helpers ----
11
+
12
+ function nodeName(node: SyntaxNodeLike): string {
13
+ // Most declaration nodes expose "name" as a named field
14
+ const nameNode = node.childForFieldName('name');
15
+ return nameNode?.text ?? '';
16
+ }
17
+
18
+ function loc(node: SyntaxNodeLike): string {
19
+ return `L${node.startPosition.row + 1}`;
20
+ }
21
+
22
+ // ---- tree-sitter extraction ----
23
+
24
+ const CLASS_TYPES = new Set([
25
+ 'class_declaration',
26
+ 'abstract_class_declaration',
27
+ ]);
28
+
29
+ const FUNCTION_TYPES = new Set([
30
+ 'function_declaration',
31
+ 'generator_function_declaration',
32
+ ]);
33
+
34
+ function extractWithTreeSitter(
35
+ filePath: string,
36
+ content: string,
37
+ language: string,
38
+ ): ExtractionResult {
39
+ const nodes: GraphNode[] = [];
40
+ const edges: GraphEdge[] = [];
41
+ const errors: string[] = [];
42
+
43
+ const parser = tryLoadParser(language);
44
+ if (!parser) {
45
+ return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
46
+ }
47
+
48
+ let tree: { rootNode: SyntaxNodeLike };
49
+ try {
50
+ tree = parser.parse(content);
51
+ } catch (err) {
52
+ errors.push(`tree-sitter parse error in ${filePath}: ${String(err)}`);
53
+ return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
54
+ }
55
+
56
+ // Track current class/method context for call-edge attribution
57
+ const classStack: string[] = [];
58
+ const methodStack: string[] = [];
59
+
60
+ const addNode = (name: string, n: SyntaxNodeLike): void => {
61
+ if (!name) return;
62
+ nodes.push({
63
+ id: name,
64
+ label: name,
65
+ fileType: 'code',
66
+ sourceFile: filePath,
67
+ sourceLocation: loc(n),
68
+ });
69
+ };
70
+
71
+ walk(tree.rootNode, (n) => {
72
+ // ---- classes ----
73
+ if (CLASS_TYPES.has(n.type)) {
74
+ const name = nodeName(n);
75
+ addNode(name, n);
76
+ if (name) classStack.push(name);
77
+
78
+ // heritage: extends / implements
79
+ for (const child of n.children) {
80
+ if (child.type === 'class_heritage') {
81
+ for (const clause of child.children) {
82
+ if (clause.type !== 'extends_clause' && clause.type !== 'implements_clause') continue;
83
+ const relation = clause.type === 'extends_clause' ? 'extends' : 'implements';
84
+ // Each type_identifier inside the clause is a target
85
+ for (const target of clause.children) {
86
+ if (
87
+ target.type === 'type_identifier' ||
88
+ target.type === 'identifier'
89
+ ) {
90
+ edges.push({
91
+ source: name,
92
+ target: target.text,
93
+ relation,
94
+ confidence: 'EXTRACTED',
95
+ sourceFile: filePath,
96
+ sourceLocation: loc(clause),
97
+ });
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ return;
104
+ }
105
+
106
+ // ---- functions (top-level) ----
107
+ if (FUNCTION_TYPES.has(n.type)) {
108
+ const name = nodeName(n);
109
+ addNode(name, n);
110
+ if (name) methodStack.push(name);
111
+ return;
112
+ }
113
+
114
+ // ---- method definitions (inside class body) ----
115
+ if (n.type === 'method_definition' || n.type === 'public_field_definition') {
116
+ const name = nodeName(n);
117
+ // Qualify name with class if available
118
+ const qualifiedName = classStack.length > 0 ? `${classStack[classStack.length - 1]}.${name}` : name;
119
+ addNode(qualifiedName, n);
120
+ if (qualifiedName) methodStack.push(qualifiedName);
121
+ return;
122
+ }
123
+
124
+ // ---- interfaces ----
125
+ if (n.type === 'interface_declaration') {
126
+ const name = nodeName(n);
127
+ addNode(name, n);
128
+
129
+ // interface X extends Y
130
+ for (const child of n.children) {
131
+ if (child.type === 'extends_type_clause' || child.type === 'extends_clause') {
132
+ for (const target of child.children) {
133
+ if (target.type === 'type_identifier' || target.type === 'identifier') {
134
+ edges.push({
135
+ source: name,
136
+ target: target.text,
137
+ relation: 'extends',
138
+ confidence: 'EXTRACTED',
139
+ sourceFile: filePath,
140
+ sourceLocation: loc(child),
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ return;
147
+ }
148
+
149
+ // ---- type aliases ----
150
+ if (n.type === 'type_alias_declaration') {
151
+ const name = nodeName(n);
152
+ addNode(name, n);
153
+ return;
154
+ }
155
+
156
+ // ---- import statements ----
157
+ if (n.type === 'import_statement') {
158
+ // Extract the module specifier
159
+ const moduleSpecifier = n.childForFieldName('source');
160
+ const moduleText = moduleSpecifier
161
+ ? moduleSpecifier.text.replace(/^['"]|['"]$/g, '')
162
+ : '';
163
+
164
+ // Extract named specifiers: import { A, B } from '...'
165
+ for (const child of n.children) {
166
+ if (child.type === 'named_imports' || child.type === 'import_clause') {
167
+ for (const spec of child.children) {
168
+ if (spec.type === 'import_specifier') {
169
+ const importedName = spec.childForFieldName('name')?.text ?? spec.text;
170
+ if (importedName) {
171
+ edges.push({
172
+ source: filePath,
173
+ target: importedName,
174
+ relation: 'imports',
175
+ confidence: 'EXTRACTED',
176
+ sourceFile: filePath,
177
+ sourceLocation: loc(n),
178
+ });
179
+ }
180
+ }
181
+ }
182
+ }
183
+ // Default import: import X from '...'
184
+ if (child.type === 'import_clause') {
185
+ const defaultId = child.childForFieldName('name') ?? (
186
+ child.children.find(c => c.type === 'identifier') ?? null
187
+ );
188
+ if (defaultId) {
189
+ edges.push({
190
+ source: filePath,
191
+ target: defaultId.text,
192
+ relation: 'imports',
193
+ confidence: 'EXTRACTED',
194
+ sourceFile: filePath,
195
+ sourceLocation: loc(n),
196
+ });
197
+ }
198
+ }
199
+ }
200
+
201
+ // Always emit a file-level import edge to the module path
202
+ if (moduleText) {
203
+ edges.push({
204
+ source: basename(filePath),
205
+ target: moduleText,
206
+ relation: 'imports',
207
+ confidence: 'EXTRACTED',
208
+ sourceFile: filePath,
209
+ sourceLocation: loc(n),
210
+ });
211
+ }
212
+ return;
213
+ }
214
+
215
+ // ---- call expressions ----
216
+ if (n.type === 'call_expression') {
217
+ const fnNode = n.childForFieldName('function');
218
+ if (!fnNode) return;
219
+
220
+ // Resolve callee name (may be `a.b()` or plain `foo()`)
221
+ let calleeName: string;
222
+ if (fnNode.type === 'member_expression') {
223
+ calleeName = fnNode.text;
224
+ } else {
225
+ calleeName = fnNode.text;
226
+ }
227
+
228
+ // Attribute to deepest known method context
229
+ const caller =
230
+ methodStack[methodStack.length - 1] ??
231
+ classStack[classStack.length - 1] ??
232
+ basename(filePath);
233
+
234
+ if (calleeName && caller && calleeName !== caller) {
235
+ edges.push({
236
+ source: caller,
237
+ target: calleeName,
238
+ relation: 'calls',
239
+ confidence: 'INFERRED',
240
+ confidenceScore: 0.8,
241
+ sourceFile: filePath,
242
+ sourceLocation: loc(n),
243
+ });
244
+ }
245
+ }
246
+ });
247
+
248
+ return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
249
+ }
250
+
251
+ // ---- regex fallback ----
252
+
253
+ function extractWithRegex(filePath: string, content: string): ExtractionResult {
254
+ const nodes: GraphNode[] = [];
255
+ const edges: GraphEdge[] = [];
256
+
257
+ const lines = content.split('\n');
258
+
259
+ lines.forEach((line, idx) => {
260
+ const location = `L${idx + 1}`;
261
+
262
+ // class
263
+ const classMatch = line.match(/^(export\s+)?(abstract\s+)?class\s+(\w+)/);
264
+ if (classMatch) {
265
+ const name = classMatch[3];
266
+ nodes.push({ id: name, label: name, fileType: 'code', sourceFile: filePath, sourceLocation: location });
267
+
268
+ // extends
269
+ const extendsMatch = line.match(/\bextends\s+(\w+)/);
270
+ if (extendsMatch) {
271
+ edges.push({ source: name, target: extendsMatch[1], relation: 'extends', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
272
+ }
273
+ // implements
274
+ const implementsMatch = line.match(/\bimplements\s+([\w,\s]+)/);
275
+ if (implementsMatch) {
276
+ for (const impl of implementsMatch[1].split(',')) {
277
+ const implName = impl.trim().split(/\s/)[0];
278
+ if (implName) {
279
+ edges.push({ source: name, target: implName, relation: 'implements', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ // interface
286
+ const ifaceMatch = line.match(/^(export\s+)?interface\s+(\w+)/);
287
+ if (ifaceMatch) {
288
+ const name = ifaceMatch[2];
289
+ nodes.push({ id: name, label: name, fileType: 'code', sourceFile: filePath, sourceLocation: location });
290
+ }
291
+
292
+ // type alias
293
+ const typeMatch = line.match(/^(export\s+)?type\s+(\w+)\s*=/);
294
+ if (typeMatch) {
295
+ const name = typeMatch[2];
296
+ nodes.push({ id: name, label: name, fileType: 'code', sourceFile: filePath, sourceLocation: location });
297
+ }
298
+
299
+ // function
300
+ const funcMatch = line.match(/^(export\s+)?(default\s+)?(async\s+)?function\s+(\w+)/);
301
+ if (funcMatch) {
302
+ const name = funcMatch[4];
303
+ nodes.push({ id: name, label: name, fileType: 'code', sourceFile: filePath, sourceLocation: location });
304
+ }
305
+
306
+ // import
307
+ const importMatch = line.match(/^import\s+.*from\s+['"]([^'"]+)['"]/);
308
+ if (importMatch) {
309
+ edges.push({ source: basename(filePath), target: importMatch[1], relation: 'imports', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
310
+
311
+ // Named specifiers
312
+ const namedMatch = line.match(/\{([^}]+)\}/);
313
+ if (namedMatch) {
314
+ for (const spec of namedMatch[1].split(',')) {
315
+ const specName = spec.trim().split(/\s+as\s+/)[0].trim();
316
+ if (specName) {
317
+ edges.push({ source: filePath, target: specName, relation: 'imports', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
318
+ }
319
+ }
320
+ }
321
+ }
322
+ });
323
+
324
+ return { nodes, edges, filesProcessed: 1, fromCache: 0, errors: [] };
325
+ }
326
+
327
+ // ---- extractor implementation ----
328
+
329
+ export const typescriptExtractor: LanguageExtractor = {
330
+ language: 'typescript',
331
+ extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
332
+
333
+ extract(filePath: string, content: string): ExtractionResult {
334
+ const lang = filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? 'tsx' : 'typescript';
335
+
336
+ const tsResult = extractWithTreeSitter(filePath, content, lang);
337
+ if (tsResult.nodes.length > 0 || tsResult.edges.length > 0 || tsResult.errors.length > 0) {
338
+ return tsResult;
339
+ }
340
+
341
+ // tree-sitter not available or produced nothing — fall back to regex
342
+ return extractWithRegex(filePath, content);
343
+ },
344
+ };