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,228 +1,460 @@
1
- import type { ExtractedDependency, ExtractedSymbol, ParsedFileAST, Visibility } from "../../../types/index.ts";
2
- import { findPythonBlockEnd, calculatePythonNesting } from "../boundary-tracker.ts";
3
- import type { CodeExtractor } from "./base.ts";
4
-
5
- const PYTHON_STDLIB_AND_EXTERNALS = new Set([
6
- "os",
7
- "sys",
8
- "json",
9
- "math",
10
- "typing",
11
- "re",
12
- "datetime",
13
- "time",
14
- "collections",
15
- "itertools",
16
- "functools",
17
- "pathlib",
18
- "io",
19
- "logging",
20
- "subprocess",
21
- "threading",
22
- "multiprocessing",
23
- "asyncio",
24
- "abc",
25
- "copy",
26
- "enum",
27
- "dataclasses",
28
- "uuid",
29
- "hashlib",
30
- "random",
31
- "socket",
32
- "unittest",
33
- // Common 3rd party frameworks
34
- "pydantic",
35
- "fastapi",
36
- "django",
37
- "flask",
38
- "sqlalchemy",
39
- "celery",
40
- "numpy",
41
- "pandas",
42
- "requests",
43
- "httpx",
44
- "pytest",
45
- "starlette",
46
- "alembic",
47
- "redis",
48
- ]);
49
-
50
- export class PythonExtractor implements CodeExtractor {
51
- public canHandle(filePath: string): boolean {
52
- return /\.py$/i.test(filePath);
53
- }
54
-
55
- public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
56
- const symbols: ExtractedSymbol[] = [];
57
- const dependencies: ExtractedDependency[] = [];
58
-
59
- const lines = content.split("\n");
60
-
61
- // 1. Extract Python Imports
62
- for (let i = 0; i < lines.length; i++) {
63
- const line = lines[i];
64
- const trimmed = line.trim();
65
- const lineNum = i + 1;
66
-
67
- // Skip comments
68
- if (trimmed.startsWith("#")) continue;
69
-
70
- // Pattern A: from <module> import <symbols>
71
- const fromMatch = /^from\s+([.\w]+)\s+import\s+([*\w\s,()]+)/.exec(trimmed);
72
- if (fromMatch) {
73
- const modulePath = fromMatch[1];
74
- const isRelative = modulePath.startsWith(".");
75
- const topPackage = isRelative ? "" : modulePath.split(".")[0];
76
- const isExternal = !isRelative && PYTHON_STDLIB_AND_EXTERNALS.has(topPackage);
77
-
78
- dependencies.push({
79
- target: modulePath,
80
- statement: trimmed,
81
- line_number: lineNum,
82
- is_external: isExternal,
83
- });
84
- continue;
85
- }
86
-
87
- // Pattern B: import <module> [as <alias>]
88
- const importMatch = /^import\s+([.\w]+)(?:\s+as\s+\w+)?/.exec(trimmed);
89
- if (importMatch) {
90
- const modulePath = importMatch[1];
91
- const topPackage = modulePath.split(".")[0];
92
- const isExternal = PYTHON_STDLIB_AND_EXTERNALS.has(topPackage);
93
-
94
- dependencies.push({
95
- target: modulePath,
96
- statement: trimmed,
97
- line_number: lineNum,
98
- is_external: isExternal,
99
- });
100
- }
101
- }
102
-
103
- // 2. Extract Python Classes and Functions
104
- let currentClass: string | null = null;
105
- let currentClassIndent = -1;
106
-
107
- for (let i = 0; i < lines.length; i++) {
108
- const line = lines[i];
109
- const trimmed = line.trim();
110
- const lineNum = i + 1;
111
-
112
- if (!trimmed || trimmed.startsWith("#")) continue;
113
-
114
- const indentMatch = /^(\s*)/.exec(line);
115
- const indent = indentMatch ? indentMatch[1].length : 0;
116
-
117
- // Check if we exited current class scope
118
- if (currentClass !== null && indent <= currentClassIndent) {
119
- currentClass = null;
120
- currentClassIndent = -1;
121
- }
122
-
123
- // Class Definition
124
- const classMatch = /^class\s+([A-Za-z0-9_]+)(?:\s*\([^)]*\))?\s*:/.exec(trimmed);
125
- if (classMatch) {
126
- const className = classMatch[1];
127
- currentClass = className;
128
- currentClassIndent = indent;
129
-
130
- const visibility: Visibility = className.startsWith("__")
131
- ? "private"
132
- : className.startsWith("_")
133
- ? "protected"
134
- : "public";
135
-
136
- const endLine = findPythonBlockEnd(lines, i);
137
- symbols.push({
138
- name: className,
139
- kind: "class",
140
- signature: trimmed.replace(/:$/, "").trim(),
141
- visibility,
142
- line_start: lineNum,
143
- line_end: endLine,
144
- line_count: Math.max(1, endLine - lineNum + 1),
145
- });
146
- continue;
147
- }
148
-
149
- // Class Attributes or Pydantic fields inside class
150
- if (currentClass !== null && indent > currentClassIndent) {
151
- const attrMatch = /^([A-Za-z0-9_]+)\s*(?::\s*([^=]+))?(?:\s*=\s*(.+))?$/.exec(trimmed);
152
- if (
153
- attrMatch &&
154
- !trimmed.startsWith("def ") &&
155
- !trimmed.startsWith("async def ") &&
156
- !["return", "pass", "raise", "assert", "import", "from", "yield"].includes(attrMatch[1])
157
- ) {
158
- const attrName = attrMatch[1];
159
- const visibility: Visibility = attrName.startsWith("__")
160
- ? "private"
161
- : attrName.startsWith("_")
162
- ? "protected"
163
- : "public";
164
-
165
- symbols.push({
166
- name: `${currentClass}::${attrName}`,
167
- kind: "property",
168
- signature: trimmed,
169
- visibility,
170
- line_start: lineNum,
171
- line_end: lineNum,
172
- line_count: 1,
173
- });
174
- continue;
175
- }
176
- }
177
-
178
- // Module-level UPPERCASE constants: MAX_RETRIES = 5
179
- if (currentClass === null && indent === 0) {
180
- const constMatch = /^([A-Z][A-Z0-9_]*)\s*(?::\s*([^=]+))?\s*=\s*(.+)$/.exec(trimmed);
181
- if (constMatch) {
182
- const constName = constMatch[1];
183
- symbols.push({
184
- name: constName,
185
- kind: "property",
186
- signature: trimmed,
187
- visibility: "public",
188
- line_start: lineNum,
189
- line_end: lineNum,
190
- line_count: 1,
191
- });
192
- continue;
193
- }
194
- }
195
-
196
- // Function or Method Definition
197
- const funcMatch = /^(?:async\s+)?def\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*->\s*[^:]+)?\s*:/.exec(trimmed);
198
- if (funcMatch) {
199
- const funcName = funcMatch[1];
200
- const isMethod = currentClass !== null && indent > currentClassIndent;
201
-
202
- const visibility: Visibility = funcName.startsWith("__")
203
- ? "private"
204
- : funcName.startsWith("_")
205
- ? "protected"
206
- : "public";
207
-
208
- const endLine = findPythonBlockEnd(lines, i);
209
- const nestingDepth = calculatePythonNesting(lines, i, endLine - 1);
210
- symbols.push({
211
- name: isMethod ? `${currentClass}::${funcName}` : funcName,
212
- kind: isMethod ? "method" : "function",
213
- signature: trimmed.replace(/:$/, "").trim(),
214
- visibility,
215
- line_start: lineNum,
216
- line_end: endLine,
217
- line_count: Math.max(1, endLine - lineNum + 1),
218
- nesting_depth: nestingDepth,
219
- });
220
- }
221
- }
222
-
223
- return {
224
- symbols,
225
- dependencies,
226
- };
227
- }
228
- }
1
+ import type { ExtractedDependency, ExtractedSymbol, ParsedFileAST, Visibility } from "../../../types/index.ts";
2
+ import type { CodeExtractor } from "./base.ts";
3
+
4
+ const PYTHON_STDLIB_AND_EXTERNALS = new Set([
5
+ "os",
6
+ "sys",
7
+ "json",
8
+ "math",
9
+ "typing",
10
+ "re",
11
+ "datetime",
12
+ "time",
13
+ "collections",
14
+ "itertools",
15
+ "functools",
16
+ "pathlib",
17
+ "io",
18
+ "logging",
19
+ "subprocess",
20
+ "threading",
21
+ "multiprocessing",
22
+ "asyncio",
23
+ "abc",
24
+ "copy",
25
+ "enum",
26
+ "dataclasses",
27
+ "uuid",
28
+ "hashlib",
29
+ "random",
30
+ "socket",
31
+ "unittest",
32
+ // Common 3rd party frameworks
33
+ "pydantic",
34
+ "fastapi",
35
+ "django",
36
+ "flask",
37
+ "sqlalchemy",
38
+ "celery",
39
+ "numpy",
40
+ "pandas",
41
+ "requests",
42
+ "httpx",
43
+ "pytest",
44
+ "starlette",
45
+ "alembic",
46
+ "redis",
47
+ ]);
48
+
49
+ import type Parser from "web-tree-sitter";
50
+ import {
51
+ blankCommentsAndStrings,
52
+ calculatePythonNesting,
53
+ findPythonBlockEnd,
54
+ } from "../boundary-tracker.ts";
55
+ import { calculateAstControlFlowNesting, parseWithTreeSitter } from "../wasm-loader.ts";
56
+
57
+ const PYTHON_CONTROL_FLOW_NODES = new Set([
58
+ "if_statement",
59
+ "for_statement",
60
+ "while_statement",
61
+ "try_statement",
62
+ "with_statement",
63
+ "match_statement",
64
+ "conditional_expression",
65
+ ]);
66
+
67
+ const PYTHON_FUNCTION_BOUNDARY_NODES = new Set([
68
+ "function_definition",
69
+ "lambda",
70
+ ]);
71
+
72
+ export class PythonExtractor implements CodeExtractor {
73
+ public canHandle(filePath: string): boolean {
74
+ return /\.py$/i.test(filePath);
75
+ }
76
+
77
+ public async extract(_filePath: string, content: string): Promise<ParsedFileAST> {
78
+ const astResult = await parseWithTreeSitter("python", content, (rootNode) =>
79
+ this.extractFromAst(rootNode, content)
80
+ );
81
+ if (astResult) {
82
+ return astResult;
83
+ }
84
+
85
+ return this.extractFallback(content);
86
+ }
87
+
88
+ private extractFallback(content: string): ParsedFileAST {
89
+ const symbols: ExtractedSymbol[] = [];
90
+ const dependencies: ExtractedDependency[] = [];
91
+
92
+ const lines = content.split("\n");
93
+ const cleanLines = blankCommentsAndStrings(content, { hashComments: true }).split("\n");
94
+
95
+ // 1. Extract Python Imports
96
+ for (let i = 0; i < lines.length; i++) {
97
+ const line = lines[i];
98
+ const trimmed = cleanLines[i].trim();
99
+ const rawTrimmed = line.trim();
100
+ const lineNum = i + 1;
101
+
102
+ if (!trimmed || trimmed.startsWith("#")) continue;
103
+
104
+ // Pattern A: from <module> import <symbols>
105
+ const fromMatch = /^from\s+([.\w]+)\s+import\s+([*\w\s,()]+)/.exec(trimmed);
106
+ if (fromMatch) {
107
+ const modulePath = fromMatch[1];
108
+ const importedList = fromMatch[2]
109
+ .replace(/[()]/g, " ")
110
+ .split(",")
111
+ .map((s) => s.trim().split(/\s+as\s+/)[0].trim())
112
+ .filter((s) => Boolean(s) && s !== "*");
113
+ const isRelative = modulePath.startsWith(".");
114
+ const topPackage = isRelative ? "" : modulePath.split(".")[0];
115
+ const isExternal = !isRelative && PYTHON_STDLIB_AND_EXTERNALS.has(topPackage);
116
+
117
+ dependencies.push({
118
+ target: modulePath,
119
+ statement: rawTrimmed,
120
+ line_number: lineNum,
121
+ is_external: isExternal,
122
+ ...(importedList.length > 0 ? { imported_symbols: importedList } : {}),
123
+ });
124
+ continue;
125
+ }
126
+
127
+ // Pattern B: import <module> [as <alias>]
128
+ const importMatch = /^import\s+([.\w]+)(?:\s+as\s+(\w+))?/.exec(trimmed);
129
+ if (importMatch) {
130
+ const modulePath = importMatch[1];
131
+ const alias = importMatch[2];
132
+ const topPackage = modulePath.split(".")[0];
133
+ const isExternal = PYTHON_STDLIB_AND_EXTERNALS.has(topPackage);
134
+ const shortName = alias || modulePath.split(".").pop() || modulePath;
135
+
136
+ dependencies.push({
137
+ target: modulePath,
138
+ statement: rawTrimmed,
139
+ line_number: lineNum,
140
+ is_external: isExternal,
141
+ imported_symbols: [shortName],
142
+ });
143
+ }
144
+ }
145
+
146
+
147
+ // 2. Extract Python Classes and Functions
148
+ let currentClass: string | null = null;
149
+ let currentClassIndent = -1;
150
+
151
+ for (let i = 0; i < lines.length; i++) {
152
+ const line = lines[i];
153
+ const trimmed = line.trim();
154
+ const lineNum = i + 1;
155
+
156
+ if (!trimmed || trimmed.startsWith("#")) continue;
157
+
158
+ const indentMatch = /^(\s*)/.exec(line);
159
+ const indent = indentMatch ? indentMatch[1].length : 0;
160
+
161
+ // Check if we exited current class scope
162
+ if (currentClass !== null && indent <= currentClassIndent) {
163
+ currentClass = null;
164
+ currentClassIndent = -1;
165
+ }
166
+
167
+ // Class Definition
168
+ const classMatch = /^class\s+([A-Za-z0-9_]+)(?:\s*\([^)]*\))?\s*:/.exec(trimmed);
169
+ if (classMatch) {
170
+ const className = classMatch[1];
171
+ currentClass = className;
172
+ currentClassIndent = indent;
173
+
174
+ const visibility: Visibility = className.startsWith("__")
175
+ ? "private"
176
+ : className.startsWith("_")
177
+ ? "protected"
178
+ : "public";
179
+
180
+ const endLine = findPythonBlockEnd(lines, i);
181
+ symbols.push({
182
+ name: className,
183
+ kind: "class",
184
+ signature: trimmed.replace(/:$/, "").trim(),
185
+ visibility,
186
+ line_start: lineNum,
187
+ line_end: endLine,
188
+ line_count: Math.max(1, endLine - lineNum + 1),
189
+ });
190
+ continue;
191
+ }
192
+
193
+ // Class Attributes or Pydantic fields inside class
194
+ if (currentClass !== null && indent > currentClassIndent) {
195
+ const attrMatch = /^([A-Za-z0-9_]+)\s*(?::\s*([^=]+))?(?:\s*=\s*(.+))?$/.exec(trimmed);
196
+ if (
197
+ attrMatch &&
198
+ !trimmed.startsWith("def ") &&
199
+ !trimmed.startsWith("async def ") &&
200
+ !["return", "pass", "raise", "assert", "import", "from", "yield"].includes(attrMatch[1])
201
+ ) {
202
+ const attrName = attrMatch[1];
203
+ const visibility: Visibility = attrName.startsWith("__")
204
+ ? "private"
205
+ : attrName.startsWith("_")
206
+ ? "protected"
207
+ : "public";
208
+
209
+ symbols.push({
210
+ name: `${currentClass}::${attrName}`,
211
+ kind: "property",
212
+ signature: trimmed,
213
+ visibility,
214
+ line_start: lineNum,
215
+ line_end: lineNum,
216
+ line_count: 1,
217
+ });
218
+ continue;
219
+ }
220
+ }
221
+
222
+ // Module-level UPPERCASE constants: MAX_RETRIES = 5
223
+ if (currentClass === null && indent === 0) {
224
+ const constMatch = /^([A-Z][A-Z0-9_]*)\s*(?::\s*([^=]+))?\s*=\s*(.+)$/.exec(trimmed);
225
+ if (constMatch) {
226
+ const constName = constMatch[1];
227
+ symbols.push({
228
+ name: constName,
229
+ kind: "property",
230
+ signature: trimmed,
231
+ visibility: "public",
232
+ line_start: lineNum,
233
+ line_end: lineNum,
234
+ line_count: 1,
235
+ });
236
+ continue;
237
+ }
238
+ }
239
+
240
+ // Function or Method Definition
241
+ const funcMatch = /^(?:async\s+)?def\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*->\s*[^:]+)?\s*:/.exec(trimmed);
242
+ if (funcMatch) {
243
+ const funcName = funcMatch[1];
244
+ const isMethod = currentClass !== null && indent > currentClassIndent;
245
+
246
+ const visibility: Visibility = funcName.startsWith("__")
247
+ ? "private"
248
+ : funcName.startsWith("_")
249
+ ? "protected"
250
+ : "public";
251
+
252
+ const endLine = findPythonBlockEnd(cleanLines, i);
253
+ const nestingDepth = calculatePythonNesting(cleanLines, i, endLine - 1);
254
+ symbols.push({
255
+ name: isMethod ? `${currentClass}::${funcName}` : funcName,
256
+ kind: isMethod ? "method" : "function",
257
+ signature: trimmed.replace(/:$/, "").trim(),
258
+ visibility,
259
+ line_start: lineNum,
260
+ line_end: endLine,
261
+ line_count: Math.max(1, endLine - lineNum + 1),
262
+ nesting_depth: nestingDepth,
263
+ });
264
+ }
265
+ }
266
+
267
+ return {
268
+ symbols,
269
+ dependencies,
270
+ };
271
+ }
272
+
273
+ private extractFromAst(rootNode: Parser.SyntaxNode, content: string): ParsedFileAST {
274
+ const symbols: ExtractedSymbol[] = [];
275
+ const dependencies: ExtractedDependency[] = [];
276
+ const lines = content.split("\n");
277
+
278
+ const getVisibility = (name: string): Visibility =>
279
+ name.startsWith("__") ? "private" : name.startsWith("_") ? "protected" : "public";
280
+
281
+ const processNode = (node: Parser.SyntaxNode, currentClass: string | null): void => {
282
+ if (node.type === "import_from_statement") {
283
+ const modNode =
284
+ node.childForFieldName("module_name") ??
285
+ node.namedChildren.find((c) => c.type === "dotted_name" || c.type === "relative_import");
286
+ if (modNode) {
287
+ const modulePath = modNode.text.trim();
288
+ const isRelative = modulePath.startsWith(".");
289
+ const topPackage = isRelative ? "" : modulePath.split(".")[0];
290
+ const isExternal = !isRelative && PYTHON_STDLIB_AND_EXTERNALS.has(topPackage);
291
+ const importedSymbols: string[] = [];
292
+
293
+ for (const child of node.namedChildren) {
294
+ if (child.equals(modNode)) continue;
295
+ if (child.type === "dotted_name") {
296
+ importedSymbols.push(child.text.trim());
297
+ } else if (child.type === "aliased_import") {
298
+ const orig = child.childForFieldName("name") ?? child.namedChildren[0];
299
+ const alias = child.childForFieldName("alias") ?? child.namedChildren[1];
300
+ if (orig?.text) importedSymbols.push(orig.text.trim());
301
+ if (alias?.text) importedSymbols.push(alias.text.trim());
302
+ }
303
+ }
304
+
305
+ dependencies.push({
306
+ target: modulePath,
307
+ statement: (lines[node.startPosition.row] ?? node.text).trim(),
308
+ line_number: node.startPosition.row + 1,
309
+ is_external: isExternal,
310
+ ...(importedSymbols.length > 0 ? { imported_symbols: Array.from(new Set(importedSymbols)) } : {}),
311
+ });
312
+ }
313
+ return;
314
+ }
315
+
316
+ if (node.type === "import_statement") {
317
+ for (const child of node.namedChildren) {
318
+ if (child.type === "dotted_name") {
319
+ const modulePath = child.text.trim();
320
+ const topPackage = modulePath.split(".")[0];
321
+ const shortName = modulePath.split(".").pop() || modulePath;
322
+ dependencies.push({
323
+ target: modulePath,
324
+ statement: (lines[node.startPosition.row] ?? node.text).trim(),
325
+ line_number: node.startPosition.row + 1,
326
+ is_external: PYTHON_STDLIB_AND_EXTERNALS.has(topPackage),
327
+ imported_symbols: [shortName],
328
+ });
329
+ } else if (child.type === "aliased_import") {
330
+ const orig = child.childForFieldName("name") ?? child.namedChildren[0];
331
+ const alias = child.childForFieldName("alias") ?? child.namedChildren[1];
332
+ if (orig) {
333
+ const modulePath = orig.text.trim();
334
+ const topPackage = modulePath.split(".")[0];
335
+ const shortName = alias?.text?.trim() || modulePath.split(".").pop() || modulePath;
336
+ dependencies.push({
337
+ target: modulePath,
338
+ statement: (lines[node.startPosition.row] ?? node.text).trim(),
339
+ line_number: node.startPosition.row + 1,
340
+ is_external: PYTHON_STDLIB_AND_EXTERNALS.has(topPackage),
341
+ imported_symbols: [shortName],
342
+ });
343
+ }
344
+ }
345
+ }
346
+ return;
347
+ }
348
+
349
+ if (node.type === "decorated_definition") {
350
+ const defNode =
351
+ node.childForFieldName("definition") ??
352
+ node.namedChildren.find((c) => c.type === "class_definition" || c.type === "function_definition");
353
+ if (defNode) {
354
+ processNode(defNode, currentClass);
355
+ }
356
+ return;
357
+ }
358
+
359
+ if (node.type === "class_definition") {
360
+ const nameNode = node.childForFieldName("name") ?? node.namedChildren.find((c) => c.type === "identifier");
361
+ if (!nameNode) return;
362
+
363
+ const className = nameNode.text;
364
+ const lineStart = node.startPosition.row + 1;
365
+ const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
366
+ const headerLine = (lines[node.startPosition.row] ?? "").trim().replace(/:$/, "").trim();
367
+
368
+ symbols.push({
369
+ name: className,
370
+ kind: "class",
371
+ signature: headerLine || `class ${className}`,
372
+ visibility: getVisibility(className),
373
+ line_start: lineStart,
374
+ line_end: lineEnd,
375
+ line_count: Math.max(1, lineEnd - lineStart + 1),
376
+ });
377
+
378
+ const bodyNode = node.childForFieldName("body") ?? node.namedChildren.find((c) => c.type === "block");
379
+ if (bodyNode) {
380
+ for (const stmt of bodyNode.namedChildren) {
381
+ processNode(stmt, className);
382
+ }
383
+ }
384
+ return;
385
+ }
386
+
387
+ if (node.type === "function_definition") {
388
+ const nameNode = node.childForFieldName("name") ?? node.namedChildren.find((c) => c.type === "identifier");
389
+ if (!nameNode) return;
390
+
391
+ const funcName = nameNode.text;
392
+ const isMethod = currentClass !== null;
393
+ const lineStart = node.startPosition.row + 1;
394
+ const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
395
+ const headerLine = (lines[node.startPosition.row] ?? "").trim().replace(/:$/, "").trim();
396
+ const nestingDepth = calculateAstControlFlowNesting(
397
+ node,
398
+ PYTHON_CONTROL_FLOW_NODES,
399
+ PYTHON_FUNCTION_BOUNDARY_NODES
400
+ );
401
+
402
+ symbols.push({
403
+ name: isMethod ? `${currentClass}::${funcName}` : funcName,
404
+ kind: isMethod ? "method" : "function",
405
+ signature: headerLine || `def ${funcName}()`,
406
+ visibility: getVisibility(funcName),
407
+ line_start: lineStart,
408
+ line_end: lineEnd,
409
+ line_count: Math.max(1, lineEnd - lineStart + 1),
410
+ nesting_depth: nestingDepth,
411
+ });
412
+ return;
413
+ }
414
+
415
+ if (node.type === "expression_statement") {
416
+ const assignNode =
417
+ node.namedChildren.find((c) => c.type === "assignment") ??
418
+ (node.firstNamedChild?.type === "assignment" ? node.firstNamedChild : null);
419
+ if (assignNode) {
420
+ const leftNode = assignNode.childForFieldName("left") ?? assignNode.namedChildren[0];
421
+ if (leftNode && leftNode.type === "identifier") {
422
+ const varName = leftNode.text.trim();
423
+ const lineStart = assignNode.startPosition.row + 1;
424
+ const lineEnd = Math.max(lineStart, assignNode.endPosition.row + 1);
425
+ const rawLine = (lines[assignNode.startPosition.row] ?? assignNode.text).trim();
426
+
427
+ if (currentClass !== null) {
428
+ symbols.push({
429
+ name: `${currentClass}::${varName}`,
430
+ kind: "property",
431
+ signature: rawLine,
432
+ visibility: getVisibility(varName),
433
+ line_start: lineStart,
434
+ line_end: lineEnd,
435
+ line_count: Math.max(1, lineEnd - lineStart + 1),
436
+ });
437
+ } else if (/^[A-Z][A-Z0-9_]*$/.test(varName)) {
438
+ symbols.push({
439
+ name: varName,
440
+ kind: "property",
441
+ signature: rawLine,
442
+ visibility: "public",
443
+ line_start: lineStart,
444
+ line_end: lineEnd,
445
+ line_count: Math.max(1, lineEnd - lineStart + 1),
446
+ });
447
+ }
448
+ }
449
+ }
450
+ }
451
+ };
452
+
453
+ for (const child of rootNode.namedChildren) {
454
+ processNode(child, null);
455
+ }
456
+
457
+ return { symbols, dependencies };
458
+ }
459
+ }
460
+