septum 0.1.4 → 0.1.6

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 (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -105
  3. package/bin/septum.ts +4 -4
  4. package/package.json +63 -63
  5. package/src/cli/commands/check.ts +161 -161
  6. package/src/cli/commands/hook.ts +216 -188
  7. package/src/cli/commands/ingest.ts +29 -29
  8. package/src/cli/commands/init.ts +213 -211
  9. package/src/cli/commands/locate.ts +61 -61
  10. package/src/cli/commands/query.ts +104 -64
  11. package/src/cli/commands/serve.ts +5 -5
  12. package/src/cli/commands/slice.ts +50 -48
  13. package/src/cli/commands/sync.ts +31 -31
  14. package/src/cli/index.ts +220 -210
  15. package/src/cli/templates/hooks.ts +75 -75
  16. package/src/cli/templates/rules.ts +50 -38
  17. package/src/cli/templates/skill.ts +65 -69
  18. package/src/core/boundary/evaluator.ts +429 -210
  19. package/src/core/config/loader.ts +117 -97
  20. package/src/core/config/schema.ts +39 -39
  21. package/src/core/database/client.ts +44 -44
  22. package/src/core/database/repositories/dependency.repository.ts +552 -349
  23. package/src/core/database/repositories/domain.repository.ts +275 -198
  24. package/src/core/database/repositories/file.repository.ts +117 -106
  25. package/src/core/database/repositories/meta.repository.ts +33 -33
  26. package/src/core/database/repositories/symbol.repository.ts +482 -443
  27. package/src/core/database/repository.ts +281 -260
  28. package/src/core/database/schema.ts +154 -153
  29. package/src/core/discovery/topology-detector.ts +486 -486
  30. package/src/core/ingestion/hasher.ts +5 -5
  31. package/src/core/ingestion/pipeline.ts +606 -535
  32. package/src/core/parser/boundary-tracker.ts +433 -288
  33. package/src/core/parser/extractors/base.ts +62 -62
  34. package/src/core/parser/extractors/blade.ts +158 -158
  35. package/src/core/parser/extractors/frontend.ts +147 -194
  36. package/src/core/parser/extractors/go.ts +408 -180
  37. package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
  38. package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
  39. package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
  40. package/src/core/parser/extractors/php.ts +415 -153
  41. package/src/core/parser/extractors/python.ts +460 -228
  42. package/src/core/parser/extractors/registry.ts +70 -70
  43. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
  44. package/src/core/parser/extractors/typescript.ts +668 -181
  45. package/src/core/parser/tree-sitter.ts +41 -41
  46. package/src/core/parser/wasm-loader.ts +271 -0
  47. package/src/core/resolver/call-graph-tracer.ts +698 -668
  48. package/src/core/resolver/module-resolver.ts +479 -342
  49. package/src/core/resolver/path-resolver.ts +147 -147
  50. package/src/core/resolver/symbol-locator.ts +543 -543
  51. package/src/core/resolver/vertical-slice-tracer.ts +323 -285
  52. package/src/core/session/session-manager.ts +56 -56
  53. package/src/core/telemetry/telemetry.ts +86 -86
  54. package/src/core/topology/environment-detector.ts +109 -6
  55. package/src/index.ts +21 -21
  56. package/src/mcp/schemas.ts +695 -286
  57. package/src/mcp/server.ts +927 -716
  58. package/src/mcp/tools/check-boundary.ts +307 -152
  59. package/src/mcp/tools/get-domain-catalog.ts +155 -121
  60. package/src/mcp/tools/get-environment-topology.ts +3 -6
  61. package/src/mcp/tools/get-feature-context.ts +227 -116
  62. package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
  63. package/src/mcp/tools/get-symbol-impact.ts +28 -26
  64. package/src/mcp/tools/get-symbol.ts +65 -63
  65. package/src/mcp/tools/locate-symbol.ts +169 -190
  66. package/src/mcp/tools/register-domain.ts +59 -64
  67. package/src/mcp/tools/register-feature.ts +91 -0
  68. package/src/mcp/tools/trace-vertical-slice.ts +74 -48
  69. package/src/types/index.ts +366 -315
  70. package/src/version.ts +3 -3
@@ -1,181 +1,668 @@
1
- import type { ExtractedDependency, ExtractedSymbol, ParsedFileAST, Visibility } from "../../../types/index.ts";
2
- import { findBraceBlockEnd, calculateControlFlowNesting } from "../boundary-tracker.ts";
3
- import type { CodeExtractor } from "./base.ts";
4
-
5
- export class TypeScriptExtractor implements CodeExtractor {
6
- public canHandle(filePath: string): boolean {
7
- return /\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(filePath);
8
- }
9
-
10
- public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
11
- const symbols: ExtractedSymbol[] = [];
12
- const dependencies: ExtractedDependency[] = [];
13
-
14
- const lines = content.split("\n");
15
-
16
- // 1. Extract Imports / Dependencies
17
- const importRegex = /(?:import\s+(?:(?:[\w*\s{},]+)\s+from\s+)?['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
18
- for (let i = 0; i < lines.length; i++) {
19
- const line = lines[i];
20
- let match: RegExpExecArray | null;
21
- while ((match = importRegex.exec(line)) !== null) {
22
- const target = match[1] || match[2];
23
- if (target) {
24
- const isExternal = !target.startsWith(".") && !target.startsWith("/");
25
- dependencies.push({
26
- target,
27
- statement: line.trim(),
28
- line_number: i + 1,
29
- is_external: isExternal,
30
- });
31
- }
32
- }
33
- }
34
-
35
- // 2. Extract Symbols (Classes, Interfaces, Types, Functions, Methods)
36
- let currentClass: string | null = null;
37
- let currentClassEndLine = 0;
38
-
39
- for (let i = 0; i < lines.length; i++) {
40
- const line = lines[i];
41
- const trimmed = line.trim();
42
- const lineNum = i + 1;
43
-
44
- if (currentClass && lineNum > currentClassEndLine) {
45
- currentClass = null;
46
- currentClassEndLine = 0;
47
- }
48
-
49
- // Class / Interface / Type definition
50
- const classMatch = /(?:export\s+)?(?:default\s+)?(?:abstract\s+)?(class|interface)\s+([A-Za-z0-9_]+)/.exec(trimmed);
51
- if (classMatch) {
52
- currentClass = classMatch[2];
53
- const endLine = findBraceBlockEnd(lines, i);
54
- currentClassEndLine = endLine;
55
- symbols.push({
56
- name: classMatch[2],
57
- kind: classMatch[1] as "class" | "interface",
58
- signature: trimmed.replace(/\{$/, "").trim(),
59
- visibility: "public",
60
- line_start: lineNum,
61
- line_end: endLine,
62
- line_count: Math.max(1, endLine - lineNum + 1),
63
- });
64
- continue;
65
- }
66
-
67
- // Type Alias
68
- const typeMatch = /(?:export\s+)?type\s+([A-Za-z0-9_]+)\s*=/.exec(trimmed);
69
- if (typeMatch) {
70
- const endLine = trimmed.endsWith(";") ? lineNum : findBraceBlockEnd(lines, i);
71
- symbols.push({
72
- name: typeMatch[1],
73
- kind: "type",
74
- signature: trimmed.replace(/;$/, "").trim(),
75
- visibility: "public",
76
- line_start: lineNum,
77
- line_end: endLine,
78
- line_count: Math.max(1, endLine - lineNum + 1),
79
- });
80
- continue;
81
- }
82
-
83
- // Enum Declaration
84
- const enumMatch = /(?:export\s+)?(?:const\s+)?enum\s+([A-Za-z0-9_]+)/.exec(trimmed);
85
- if (enumMatch) {
86
- const endLine = findBraceBlockEnd(lines, i);
87
- symbols.push({
88
- name: enumMatch[1],
89
- kind: "enum",
90
- signature: trimmed.replace(/\{$/, "").trim(),
91
- visibility: "public",
92
- line_start: lineNum,
93
- line_end: endLine,
94
- line_count: Math.max(1, endLine - lineNum + 1),
95
- });
96
- continue;
97
- }
98
-
99
- // Function Declaration
100
- const fnMatch = /(?:export\s+)?(?:async\s+)?function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(trimmed);
101
- if (fnMatch) {
102
- const name = fnMatch[1];
103
- const params = fnMatch[2];
104
- const retType = fnMatch[3] ? `: ${fnMatch[3].trim()}` : "";
105
- const endLine = findBraceBlockEnd(lines, i);
106
- const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
107
- symbols.push({
108
- name,
109
- kind: "function",
110
- signature: `${name}(${params})${retType}`,
111
- visibility: "public",
112
- line_start: lineNum,
113
- line_end: endLine,
114
- line_count: Math.max(1, endLine - lineNum + 1),
115
- nesting_depth: nestingDepth,
116
- });
117
- continue;
118
- }
119
-
120
- // Exported Constants / Variables at module level
121
- const constMatch = /^export\s+(?:const|let|var)\s+([A-Za-z0-9_]+)\s*(?::\s*([^=;]+))?\s*=/.exec(trimmed);
122
- if (constMatch) {
123
- const varName = constMatch[1];
124
- const typeStr = constMatch[2] ? `: ${constMatch[2].trim()}` : "";
125
- symbols.push({
126
- name: varName,
127
- kind: "property",
128
- signature: `const ${varName}${typeStr}`,
129
- visibility: "public",
130
- line_start: lineNum,
131
- line_end: lineNum,
132
- line_count: 1,
133
- });
134
- continue;
135
- }
136
-
137
- // Method or Property in class
138
- if (currentClass) {
139
- // Class property: public foo: string; or private bar = 1;
140
- const propMatch = /^(?:(public|protected|private)\s+)?(?:readonly\s+)?([A-Za-z0-9_]+)\s*(?:\?|\!)?\s*(?::\s*([^;={]+))?(?:\s*=[^;]+)?;\s*$/.exec(trimmed);
141
- if (propMatch && !["return", "const", "let", "var", "throw", "import"].includes(propMatch[2])) {
142
- const vis = (propMatch[1] as Visibility) || "public";
143
- const propName = propMatch[2];
144
- const typeStr = propMatch[3] ? `: ${propMatch[3].trim()}` : "";
145
- symbols.push({
146
- name: `${currentClass}::${propName}`,
147
- kind: "property",
148
- signature: `${vis} ${propName}${typeStr}`,
149
- visibility: vis,
150
- line_start: lineNum,
151
- line_end: lineNum,
152
- line_count: 1,
153
- });
154
- continue;
155
- }
156
-
157
- const methodMatch = /^(?:(public|protected|private)\s+)?(?:static\s+)?(?:async\s+)?([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(trimmed);
158
- if (methodMatch && !["if", "for", "while", "switch", "catch"].includes(methodMatch[2])) {
159
- const vis = (methodMatch[1] as Visibility) || "public";
160
- const name = methodMatch[2];
161
- const params = methodMatch[3];
162
- const retType = methodMatch[4] ? `: ${methodMatch[4].trim()}` : "";
163
- const endLine = findBraceBlockEnd(lines, i);
164
- const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
165
- symbols.push({
166
- name: `${currentClass}::${name}`,
167
- kind: "method",
168
- signature: `${vis} ${name}(${params})${retType}`,
169
- visibility: vis,
170
- line_start: lineNum,
171
- line_end: endLine,
172
- line_count: Math.max(1, endLine - lineNum + 1),
173
- nesting_depth: nestingDepth,
174
- });
175
- }
176
- }
177
- }
178
-
179
- return { symbols, dependencies };
180
- }
181
- }
1
+ import type Parser from "web-tree-sitter";
2
+ import type {
3
+ ExtractedDependency,
4
+ ExtractedSymbol,
5
+ ParsedFileAST,
6
+ SymbolKind,
7
+ Visibility,
8
+ } from "../../../types/index.ts";
9
+ import {
10
+ blankCommentsAndStrings,
11
+ calculateControlFlowNesting,
12
+ findBraceBlockEnd,
13
+ } from "../boundary-tracker.ts";
14
+ import { calculateAstControlFlowNesting, parseWithTreeSitter } from "../wasm-loader.ts";
15
+ import type { CodeExtractor } from "./base.ts";
16
+
17
+ const TS_CONTROL_FLOW_NODES = new Set([
18
+ "if_statement",
19
+ "for_statement",
20
+ "for_in_statement",
21
+ "while_statement",
22
+ "do_statement",
23
+ "switch_statement",
24
+ "try_statement",
25
+ "catch_clause",
26
+ "ternary_expression",
27
+ ]);
28
+
29
+ const TS_FUNCTION_BOUNDARY_NODES = new Set([
30
+ "function_declaration",
31
+ "generator_function_declaration",
32
+ "function_expression",
33
+ "arrow_function",
34
+ "method_definition",
35
+ ]);
36
+
37
+ export class TypeScriptExtractor implements CodeExtractor {
38
+ public canHandle(filePath: string): boolean {
39
+ return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i.test(filePath);
40
+ }
41
+
42
+ public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
43
+ const langKey = this.resolveLanguageKey(filePath);
44
+ const treeSitterResult = await parseWithTreeSitter(langKey, content, (rootNode) =>
45
+ this.extractFromAst(rootNode, content)
46
+ );
47
+
48
+ if (treeSitterResult) {
49
+ return treeSitterResult;
50
+ }
51
+
52
+ return this.extractFallback(content);
53
+ }
54
+
55
+ private resolveLanguageKey(filePath: string): string {
56
+ const lower = filePath.toLowerCase();
57
+ if (lower.endsWith(".tsx")) return "tsx";
58
+ if (/\.(js|jsx|mjs|cjs)$/.test(lower)) return "javascript";
59
+ return "typescript";
60
+ }
61
+
62
+ private extractFallback(content: string): ParsedFileAST {
63
+ const symbols: ExtractedSymbol[] = [];
64
+ const dependencies: ExtractedDependency[] = [];
65
+
66
+ const rawLines = content.split("\n");
67
+ const cleanContent = blankCommentsAndStrings(content);
68
+ const cleanLines = cleanContent.split("\n");
69
+
70
+ const importRegex =
71
+ /^\s*(?:import\s+(?:type\s+)?(?:([\w*\s{},]+)\s+from\s+)?['"]([^'"]+)['"]|(?:const|let|var)\s+([\w\s{},:$]+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)|require\s*\(\s*['"]([^'"]+)['"]\s*\))/;
72
+
73
+ for (let i = 0; i < rawLines.length; i++) {
74
+ if (!cleanLines[i]?.trim()) continue;
75
+ const line = rawLines[i];
76
+ const match = importRegex.exec(line);
77
+ if (match) {
78
+ const target = match[2] || match[4] || match[5];
79
+ const clause = match[1] || match[3] || "";
80
+ if (target) {
81
+ const importedSymbols = clause
82
+ .replace(/[{}*]/g, " ")
83
+ .split(/,|\bas\b/)
84
+ .map((s) => s.trim())
85
+ .filter((s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) && s !== "type");
86
+ const isExternal = !target.startsWith(".") && !target.startsWith("/");
87
+ dependencies.push({
88
+ target,
89
+ statement: line.trim(),
90
+ line_number: i + 1,
91
+ is_external: isExternal,
92
+ ...(importedSymbols.length > 0 ? { imported_symbols: Array.from(new Set(importedSymbols)) } : {}),
93
+ });
94
+ }
95
+ }
96
+ }
97
+
98
+ // 2. Extract Symbols using cleanLines + brace depth tracking
99
+ let currentClass: string | null = null;
100
+ let currentClassDepth = 0;
101
+ let braceDepth = 0;
102
+
103
+ for (let i = 0; i < cleanLines.length; i++) {
104
+ const cleanTrimmed = cleanLines[i].trim();
105
+ const rawTrimmed = rawLines[i].trim();
106
+ const lineNum = i + 1;
107
+
108
+ if (currentClass && braceDepth < currentClassDepth) {
109
+ currentClass = null;
110
+ currentClassDepth = 0;
111
+ }
112
+
113
+ if (cleanTrimmed && braceDepth === 0) {
114
+ const classMatch =
115
+ /^(?:export\s+)?(?:default\s+)?(?:abstract\s+)?\b(class|interface)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/.exec(
116
+ cleanTrimmed
117
+ );
118
+ if (classMatch) {
119
+ currentClass = classMatch[2];
120
+ currentClassDepth = braceDepth + 1;
121
+ const endLine = findBraceBlockEnd(cleanLines, i);
122
+ symbols.push({
123
+ name: classMatch[2],
124
+ kind: classMatch[1] as "class" | "interface",
125
+ signature: rawTrimmed.replace(/\{$/, "").trim(),
126
+ visibility: "public",
127
+ line_start: lineNum,
128
+ line_end: endLine,
129
+ line_count: Math.max(1, endLine - lineNum + 1),
130
+ });
131
+ } else {
132
+ const typeMatch = /^(?:export\s+)?\btype\s+([A-Za-z_$][A-Za-z0-9_$]*)\b\s*=/.exec(cleanTrimmed);
133
+ if (typeMatch) {
134
+ const endLine = cleanTrimmed.endsWith(";") ? lineNum : findBraceBlockEnd(cleanLines, i);
135
+ symbols.push({
136
+ name: typeMatch[1],
137
+ kind: "type",
138
+ signature: rawTrimmed.replace(/;$/, "").trim(),
139
+ visibility: "public",
140
+ line_start: lineNum,
141
+ line_end: endLine,
142
+ line_count: Math.max(1, endLine - lineNum + 1),
143
+ });
144
+ }
145
+
146
+ const enumMatch = /^(?:export\s+)?(?:const\s+)?\benum\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/.exec(cleanTrimmed);
147
+ if (enumMatch) {
148
+ const endLine = findBraceBlockEnd(cleanLines, i);
149
+ symbols.push({
150
+ name: enumMatch[1],
151
+ kind: "enum",
152
+ signature: rawTrimmed.replace(/\{$/, "").trim(),
153
+ visibility: "public",
154
+ line_start: lineNum,
155
+ line_end: endLine,
156
+ line_count: Math.max(1, endLine - lineNum + 1),
157
+ });
158
+ }
159
+
160
+ const fnMatch =
161
+ /^(?:export\s+)?(?:default\s+)?(?:async\s+)?\bfunction\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(
162
+ cleanTrimmed
163
+ );
164
+ if (fnMatch) {
165
+ const name = fnMatch[1];
166
+ const params = fnMatch[2];
167
+ const retType = fnMatch[3] ? `: ${fnMatch[3].trim()}` : "";
168
+ const endLine = findBraceBlockEnd(cleanLines, i);
169
+ const nestingDepth = calculateControlFlowNesting(rawLines, i, endLine - 1);
170
+ symbols.push({
171
+ name,
172
+ kind: "function",
173
+ signature: `${name}(${params})${retType}`,
174
+ visibility: "public",
175
+ line_start: lineNum,
176
+ line_end: endLine,
177
+ line_count: Math.max(1, endLine - lineNum + 1),
178
+ nesting_depth: nestingDepth,
179
+ });
180
+ }
181
+
182
+ const varMatch =
183
+ /^(?:export\s+)?\b(const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::\s*([^=;]+))?\s*=\s*(.*)$/.exec(
184
+ cleanTrimmed
185
+ );
186
+ if (varMatch) {
187
+ const declKind = varMatch[1];
188
+ const varName = varMatch[2];
189
+ const typeStr = varMatch[3] ? `: ${varMatch[3].trim()}` : "";
190
+ const rhs = varMatch[4].trim();
191
+ const isArrowOrFn = /^(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>|^function\b/.test(rhs);
192
+ symbols.push({
193
+ name: varName,
194
+ kind: isArrowOrFn ? "function" : declKind === "const" ? "constant" : "variable",
195
+ signature: `${declKind} ${varName}${typeStr}`,
196
+ visibility: cleanTrimmed.startsWith("export ") ? "public" : "private",
197
+ line_start: lineNum,
198
+ line_end: lineNum,
199
+ line_count: 1,
200
+ });
201
+ }
202
+ }
203
+ } else if (cleanTrimmed && currentClass && braceDepth === currentClassDepth) {
204
+ const methodMatch =
205
+ /^(?:(public|protected|private)\s+)?(?:static\s+)?(?:readonly\s+)?(?:async\s+)?\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(
206
+ cleanTrimmed
207
+ );
208
+ if (
209
+ methodMatch &&
210
+ !["if", "for", "while", "switch", "catch", "function", "return"].includes(methodMatch[2])
211
+ ) {
212
+ const vis = (methodMatch[1] as Visibility) || "public";
213
+ const name = methodMatch[2];
214
+ const params = methodMatch[3];
215
+ const retType = methodMatch[4] ? `: ${methodMatch[4].trim()}` : "";
216
+ const endLine = findBraceBlockEnd(cleanLines, i);
217
+ const nestingDepth = calculateControlFlowNesting(rawLines, i, endLine - 1);
218
+ symbols.push({
219
+ name: `${currentClass}::${name}`,
220
+ kind: "method",
221
+ signature: `${vis} ${name}(${params})${retType}`,
222
+ visibility: vis,
223
+ line_start: lineNum,
224
+ line_end: endLine,
225
+ line_count: Math.max(1, endLine - lineNum + 1),
226
+ nesting_depth: nestingDepth,
227
+ });
228
+ } else {
229
+ const propMatch =
230
+ /^(?:(public|protected|private)\s+)?(?:static\s+)?(?:readonly\s+)?\b([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:\?|\!)?\s*(?::\s*([^;={]+))?(?:\s*=[^;]+)?;\s*$/.exec(
231
+ cleanTrimmed
232
+ );
233
+ if (
234
+ propMatch &&
235
+ !["return", "const", "let", "var", "throw", "import", "export", "continue", "break"].includes(
236
+ propMatch[2]
237
+ )
238
+ ) {
239
+ const vis = (propMatch[1] as Visibility) || "public";
240
+ const propName = propMatch[2];
241
+ const typeStr = propMatch[3] ? `: ${propMatch[3].trim()}` : "";
242
+ symbols.push({
243
+ name: `${currentClass}::${propName}`,
244
+ kind: "property",
245
+ signature: `${vis} ${propName}${typeStr}`,
246
+ visibility: vis,
247
+ line_start: lineNum,
248
+ line_end: lineNum,
249
+ line_count: 1,
250
+ });
251
+ }
252
+ }
253
+ }
254
+
255
+ for (const ch of cleanLines[i]) {
256
+ if (ch === "{") braceDepth++;
257
+ else if (ch === "}") braceDepth = Math.max(0, braceDepth - 1);
258
+ }
259
+ if (currentClass && braceDepth < currentClassDepth) {
260
+ currentClass = null;
261
+ currentClassDepth = 0;
262
+ }
263
+ }
264
+
265
+ return { symbols, dependencies };
266
+ }
267
+
268
+ private extractFromAst(rootNode: Parser.SyntaxNode, content: string): ParsedFileAST {
269
+ const symbols: ExtractedSymbol[] = [];
270
+ const dependencies: ExtractedDependency[] = [];
271
+ const lines = content.split("\n");
272
+
273
+ const stripQuotes = (raw: string): string => raw.replace(/^['"`]|['"`]$/g, "");
274
+ const getHeaderSignature = (node: Parser.SyntaxNode): string => {
275
+ const firstLine = lines[node.startPosition.row] ?? "";
276
+ return firstLine.trim().replace(/\{$/, "").replace(/;$/, "").trim();
277
+ };
278
+
279
+ const walkDependencies = (node: Parser.SyntaxNode): void => {
280
+ if (node.type === "import_statement") {
281
+ const sourceNode =
282
+ node.childForFieldName("source") ??
283
+ node.namedChildren.find((c) => c.type === "string");
284
+ if (sourceNode) {
285
+ const target = stripQuotes(sourceNode.text.trim());
286
+ if (target) {
287
+ const importedSymbols = this.extractImportedSymbols(node);
288
+ const isExternal = !target.startsWith(".") && !target.startsWith("/");
289
+ dependencies.push({
290
+ target,
291
+ statement: (lines[node.startPosition.row] ?? node.text).trim(),
292
+ line_number: node.startPosition.row + 1,
293
+ is_external: isExternal,
294
+ ...(importedSymbols.length > 0 ? { imported_symbols: importedSymbols } : {}),
295
+ });
296
+ }
297
+ }
298
+ return;
299
+ }
300
+
301
+ if (node.type === "call_expression") {
302
+ const fnNode = node.childForFieldName("function") ?? node.namedChildren[0];
303
+ const argsNode = node.childForFieldName("arguments") ?? node.namedChildren.find((c) => c.type === "arguments");
304
+ if (fnNode && (fnNode.text === "require" || fnNode.type === "import") && argsNode) {
305
+ const firstArg = argsNode.namedChildren[0];
306
+ if (firstArg && (firstArg.type === "string" || firstArg.type === "template_string")) {
307
+ const target = stripQuotes(firstArg.text.trim());
308
+ if (target && !target.includes("${")) {
309
+ const importedSymbols = this.extractRequireImportedSymbols(node);
310
+ const isExternal = !target.startsWith(".") && !target.startsWith("/");
311
+ dependencies.push({
312
+ target,
313
+ statement: (lines[node.startPosition.row] ?? node.text).trim(),
314
+ line_number: node.startPosition.row + 1,
315
+ is_external: isExternal,
316
+ ...(importedSymbols.length > 0 ? { imported_symbols: importedSymbols } : {}),
317
+ });
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ for (const child of node.namedChildren) {
324
+ walkDependencies(child);
325
+ }
326
+ };
327
+
328
+ walkDependencies(rootNode);
329
+
330
+ const processTopLevelNode = (stmtNode: Parser.SyntaxNode, isExported: boolean, outerStartRow?: number): void => {
331
+ const startRow = outerStartRow ?? stmtNode.startPosition.row;
332
+ const endRow = stmtNode.endPosition.row;
333
+ const lineStart = startRow + 1;
334
+ const lineEnd = Math.max(lineStart, endRow + 1);
335
+ const lineCount = Math.max(1, lineEnd - lineStart + 1);
336
+
337
+ if (stmtNode.type === "export_statement") {
338
+ const decl =
339
+ stmtNode.childForFieldName("declaration") ??
340
+ stmtNode.namedChildren.find((c) => c.type !== "decorator" && c.type !== "export_clause");
341
+ if (decl) {
342
+ processTopLevelNode(decl, true, stmtNode.startPosition.row);
343
+ }
344
+ return;
345
+ }
346
+
347
+ if (
348
+ stmtNode.type === "class_declaration" ||
349
+ stmtNode.type === "abstract_class_declaration" ||
350
+ stmtNode.type === "interface_declaration"
351
+ ) {
352
+ const nameNode =
353
+ stmtNode.childForFieldName("name") ??
354
+ stmtNode.namedChildren.find((c) => c.type === "type_identifier" || c.type === "identifier");
355
+ if (!nameNode) return;
356
+
357
+ const className = nameNode.text;
358
+ const kind: SymbolKind = stmtNode.type === "interface_declaration" ? "interface" : "class";
359
+ symbols.push({
360
+ name: className,
361
+ kind,
362
+ signature: getHeaderSignature(stmtNode),
363
+ visibility: "public",
364
+ line_start: lineStart,
365
+ line_end: lineEnd,
366
+ line_count: lineCount,
367
+ });
368
+
369
+ const bodyNode =
370
+ stmtNode.childForFieldName("body") ??
371
+ stmtNode.namedChildren.find(
372
+ (c) => c.type === "class_body" || c.type === "interface_body" || c.type === "object_type"
373
+ );
374
+ if (bodyNode) {
375
+ this.extractClassOrInterfaceMembers(className, bodyNode, symbols);
376
+ }
377
+ return;
378
+ }
379
+
380
+ if (stmtNode.type === "type_alias_declaration" || stmtNode.type === "enum_declaration") {
381
+ const nameNode =
382
+ stmtNode.childForFieldName("name") ??
383
+ stmtNode.namedChildren.find((c) => c.type === "type_identifier" || c.type === "identifier");
384
+ if (!nameNode) return;
385
+
386
+ symbols.push({
387
+ name: nameNode.text,
388
+ kind: stmtNode.type === "enum_declaration" ? "enum" : "type",
389
+ signature: getHeaderSignature(stmtNode),
390
+ visibility: "public",
391
+ line_start: lineStart,
392
+ line_end: lineEnd,
393
+ line_count: lineCount,
394
+ });
395
+ return;
396
+ }
397
+
398
+ if (stmtNode.type === "function_declaration" || stmtNode.type === "generator_function_declaration") {
399
+ const nameNode =
400
+ stmtNode.childForFieldName("name") ??
401
+ stmtNode.namedChildren.find((c) => c.type === "identifier");
402
+ if (!nameNode) return;
403
+
404
+ const fnName = nameNode.text;
405
+ const paramsNode =
406
+ stmtNode.childForFieldName("parameters") ??
407
+ stmtNode.namedChildren.find((c) => c.type === "formal_parameters");
408
+ const retNode = stmtNode.childForFieldName("return_type");
409
+ const paramsText = paramsNode ? paramsNode.text : "()";
410
+ const retText = retNode ? `: ${retNode.text.replace(/^:\s*/, "")}` : "";
411
+ const nestingDepth = calculateAstControlFlowNesting(
412
+ stmtNode,
413
+ TS_CONTROL_FLOW_NODES,
414
+ TS_FUNCTION_BOUNDARY_NODES
415
+ );
416
+
417
+ symbols.push({
418
+ name: fnName,
419
+ kind: "function",
420
+ signature: `${fnName}${paramsText}${retText}`,
421
+ visibility: "public",
422
+ line_start: lineStart,
423
+ line_end: lineEnd,
424
+ line_count: lineCount,
425
+ nesting_depth: nestingDepth,
426
+ });
427
+ return;
428
+ }
429
+
430
+ if (stmtNode.type === "lexical_declaration" || stmtNode.type === "variable_declaration") {
431
+ this.extractVariableDeclarations(stmtNode, isExported, outerStartRow, symbols);
432
+ }
433
+ };
434
+
435
+ for (const child of rootNode.namedChildren) {
436
+ processTopLevelNode(child, false);
437
+ }
438
+
439
+ return { symbols, dependencies };
440
+ }
441
+
442
+ private extractVariableDeclarations(
443
+ stmtNode: Parser.SyntaxNode,
444
+ isExported: boolean,
445
+ outerStartRow: number | undefined,
446
+ symbols: ExtractedSymbol[]
447
+ ): void {
448
+ const declText = stmtNode.text.trimStart();
449
+ const isConst = declText.startsWith("const ") || declText.startsWith("const\t");
450
+ const declKeyword = isConst ? "const" : declText.startsWith("var ") ? "var" : "let";
451
+
452
+ for (const child of stmtNode.namedChildren) {
453
+ if (child.type !== "variable_declarator") continue;
454
+ const nameNode = child.childForFieldName("name") ?? child.namedChildren[0];
455
+ if (!nameNode || nameNode.type !== "identifier") continue;
456
+
457
+ const varName = nameNode.text;
458
+ const typeNode = child.childForFieldName("type");
459
+ const valueNode = child.childForFieldName("value");
460
+ const typeStr = typeNode ? `: ${typeNode.text.replace(/^:\s*/, "")}` : "";
461
+
462
+ const dStart = (outerStartRow ?? child.startPosition.row) + 1;
463
+ const dEnd = Math.max(dStart, child.endPosition.row + 1);
464
+ const dCount = Math.max(1, dEnd - dStart + 1);
465
+
466
+ if (
467
+ valueNode &&
468
+ (valueNode.type === "arrow_function" ||
469
+ valueNode.type === "function_expression" ||
470
+ valueNode.type === "function")
471
+ ) {
472
+ const paramsNode =
473
+ valueNode.childForFieldName("parameters") ??
474
+ valueNode.namedChildren.find((c) => c.type === "formal_parameters");
475
+ const retNode = valueNode.childForFieldName("return_type");
476
+ const paramsText = paramsNode ? paramsNode.text : "()";
477
+ const retText = retNode ? `: ${retNode.text.replace(/^:\s*/, "")}` : typeStr;
478
+ const nestingDepth = calculateAstControlFlowNesting(
479
+ valueNode,
480
+ TS_CONTROL_FLOW_NODES,
481
+ TS_FUNCTION_BOUNDARY_NODES
482
+ );
483
+
484
+ symbols.push({
485
+ name: varName,
486
+ kind: "function",
487
+ signature: `${varName}${paramsText}${retText}`,
488
+ visibility: isExported ? "public" : "private",
489
+ line_start: dStart,
490
+ line_end: dEnd,
491
+ line_count: dCount,
492
+ nesting_depth: nestingDepth,
493
+ });
494
+ } else {
495
+ symbols.push({
496
+ name: varName,
497
+ kind: isConst ? "constant" : "variable",
498
+ signature: `${declKeyword} ${varName}${typeStr}`,
499
+ visibility: isExported ? "public" : "private",
500
+ line_start: dStart,
501
+ line_end: dEnd,
502
+ line_count: dCount,
503
+ });
504
+ }
505
+ }
506
+ }
507
+
508
+ private extractClassOrInterfaceMembers(
509
+ className: string,
510
+ bodyNode: Parser.SyntaxNode,
511
+ symbols: ExtractedSymbol[]
512
+ ): void {
513
+ for (const member of bodyNode.namedChildren) {
514
+ const mStart = member.startPosition.row + 1;
515
+ const mEnd = Math.max(mStart, member.endPosition.row + 1);
516
+ const mCount = Math.max(1, mEnd - mStart + 1);
517
+
518
+ if (
519
+ member.type === "method_definition" ||
520
+ member.type === "method_signature" ||
521
+ member.type === "abstract_method_signature"
522
+ ) {
523
+ const nameNode =
524
+ member.childForFieldName("name") ??
525
+ member.namedChildren.find(
526
+ (c) =>
527
+ c.type === "property_identifier" ||
528
+ c.type === "private_property_identifier" ||
529
+ c.type === "identifier"
530
+ );
531
+ if (!nameNode) continue;
532
+
533
+ const methodName = nameNode.text;
534
+ const vis = this.extractVisibility(member, methodName);
535
+ const paramsNode =
536
+ member.childForFieldName("parameters") ??
537
+ member.namedChildren.find((c) => c.type === "formal_parameters");
538
+ const retNode = member.childForFieldName("return_type");
539
+ const paramsText = paramsNode ? paramsNode.text : "()";
540
+ const retText = retNode ? `: ${retNode.text.replace(/^:\s*/, "")}` : "";
541
+ const nestingDepth = calculateAstControlFlowNesting(
542
+ member,
543
+ TS_CONTROL_FLOW_NODES,
544
+ TS_FUNCTION_BOUNDARY_NODES
545
+ );
546
+
547
+ symbols.push({
548
+ name: `${className}::${methodName}`,
549
+ kind: "method",
550
+ signature: `${vis} ${methodName}${paramsText}${retText}`,
551
+ visibility: vis,
552
+ line_start: mStart,
553
+ line_end: mEnd,
554
+ line_count: mCount,
555
+ nesting_depth: nestingDepth,
556
+ });
557
+ continue;
558
+ }
559
+
560
+ if (
561
+ member.type === "public_field_definition" ||
562
+ member.type === "field_definition" ||
563
+ member.type === "property_signature"
564
+ ) {
565
+ const nameNode =
566
+ member.childForFieldName("name") ??
567
+ member.namedChildren.find(
568
+ (c) =>
569
+ c.type === "property_identifier" ||
570
+ c.type === "private_property_identifier" ||
571
+ c.type === "identifier"
572
+ );
573
+ if (!nameNode) continue;
574
+
575
+ const propName = nameNode.text;
576
+ const vis = this.extractVisibility(member, propName);
577
+ const typeNode = member.childForFieldName("type");
578
+ const typeStr = typeNode ? `: ${typeNode.text.replace(/^:\s*/, "")}` : "";
579
+
580
+ symbols.push({
581
+ name: `${className}::${propName}`,
582
+ kind: "property",
583
+ signature: `${vis} ${propName}${typeStr}`,
584
+ visibility: vis,
585
+ line_start: mStart,
586
+ line_end: mEnd,
587
+ line_count: mCount,
588
+ });
589
+ }
590
+ }
591
+ }
592
+
593
+ private extractVisibility(memberNode: Parser.SyntaxNode, memberName: string): Visibility {
594
+ if (memberName.startsWith("#")) return "private";
595
+ for (const child of memberNode.children) {
596
+ if (child.type === "accessibility_modifier") {
597
+ const text = child.text.trim();
598
+ if (text === "private" || text === "protected" || text === "public") {
599
+ return text;
600
+ }
601
+ }
602
+ }
603
+ return "public";
604
+ }
605
+
606
+ private extractImportedSymbols(importNode: Parser.SyntaxNode): string[] {
607
+ const names = new Set<string>();
608
+ const walkClause = (node: Parser.SyntaxNode): void => {
609
+ if (node.type === "import_specifier") {
610
+ const origName = node.childForFieldName("name") ?? node.namedChildren[0];
611
+ const aliasName = node.childForFieldName("alias");
612
+ if (origName && origName.text) names.add(origName.text.trim());
613
+ if (aliasName && aliasName.text) names.add(aliasName.text.trim());
614
+ return;
615
+ }
616
+ if (node.type === "namespace_import") {
617
+ const idNode = node.namedChildren.find((c) => c.type === "identifier");
618
+ if (idNode?.text) names.add(idNode.text.trim());
619
+ return;
620
+ }
621
+ if (node.type === "import_clause") {
622
+ for (const child of node.namedChildren) {
623
+ if (child.type === "identifier") {
624
+ names.add(child.text.trim());
625
+ } else {
626
+ walkClause(child);
627
+ }
628
+ }
629
+ return;
630
+ }
631
+ for (const child of node.namedChildren) {
632
+ if (child.type !== "string") {
633
+ walkClause(child);
634
+ }
635
+ }
636
+ };
637
+ walkClause(importNode);
638
+ return Array.from(names);
639
+ }
640
+
641
+ private extractRequireImportedSymbols(callNode: Parser.SyntaxNode): string[] {
642
+ const names = new Set<string>();
643
+ const parent = callNode.parent;
644
+ if (parent && parent.type === "variable_declarator") {
645
+ const nameNode = parent.childForFieldName("name") ?? parent.namedChildren[0];
646
+ if (nameNode) {
647
+ if (nameNode.type === "identifier") {
648
+ names.add(nameNode.text.trim());
649
+ } else if (nameNode.type === "object_pattern") {
650
+ for (const prop of nameNode.namedChildren) {
651
+ if (prop.type === "shorthand_property_identifier_pattern" || prop.type === "identifier") {
652
+ names.add(prop.text.trim());
653
+ } else if (prop.type === "pair_pattern") {
654
+ const key = prop.childForFieldName("key") ?? prop.namedChildren[0];
655
+ const val = prop.childForFieldName("value") ?? prop.namedChildren[1];
656
+ if (key?.text) names.add(key.text.trim());
657
+ if (val?.text) names.add(val.text.trim());
658
+ }
659
+ }
660
+ }
661
+ }
662
+ }
663
+ return Array.from(names);
664
+ }
665
+ }
666
+
667
+
668
+