septum 0.1.5 → 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 (44) hide show
  1. package/package.json +1 -1
  2. package/src/cli/commands/hook.ts +36 -8
  3. package/src/cli/commands/init.ts +6 -4
  4. package/src/cli/commands/query.ts +45 -5
  5. package/src/cli/commands/slice.ts +4 -2
  6. package/src/cli/index.ts +15 -5
  7. package/src/cli/templates/hooks.ts +6 -6
  8. package/src/cli/templates/rules.ts +35 -23
  9. package/src/cli/templates/skill.ts +47 -51
  10. package/src/core/boundary/evaluator.ts +198 -1
  11. package/src/core/config/loader.ts +20 -0
  12. package/src/core/database/repositories/dependency.repository.ts +249 -63
  13. package/src/core/database/repositories/domain.repository.ts +96 -39
  14. package/src/core/database/repositories/file.repository.ts +20 -19
  15. package/src/core/database/repositories/symbol.repository.ts +44 -5
  16. package/src/core/database/repository.ts +19 -2
  17. package/src/core/database/schema.ts +3 -2
  18. package/src/core/ingestion/pipeline.ts +47 -25
  19. package/src/core/parser/boundary-tracker.ts +145 -0
  20. package/src/core/parser/extractors/frontend.ts +43 -90
  21. package/src/core/parser/extractors/go.ts +200 -21
  22. package/src/core/parser/extractors/laravel-semantic.ts +18 -12
  23. package/src/core/parser/extractors/mcp-cli-semantic.ts +173 -44
  24. package/src/core/parser/extractors/nestjs-semantic.ts +20 -5
  25. package/src/core/parser/extractors/php.ts +293 -31
  26. package/src/core/parser/extractors/python.ts +242 -10
  27. package/src/core/parser/extractors/typescript.ts +604 -117
  28. package/src/core/parser/wasm-loader.ts +271 -0
  29. package/src/core/resolver/call-graph-tracer.ts +49 -17
  30. package/src/core/resolver/module-resolver.ts +87 -16
  31. package/src/core/resolver/vertical-slice-tracer.ts +33 -11
  32. package/src/core/topology/environment-detector.ts +109 -6
  33. package/src/mcp/schemas.ts +146 -29
  34. package/src/mcp/server.ts +93 -41
  35. package/src/mcp/tools/check-boundary.ts +176 -40
  36. package/src/mcp/tools/get-domain-catalog.ts +24 -14
  37. package/src/mcp/tools/get-environment-topology.ts +3 -6
  38. package/src/mcp/tools/get-feature-context.ts +35 -20
  39. package/src/mcp/tools/get-symbol-hotspots.ts +4 -10
  40. package/src/mcp/tools/locate-symbol.ts +12 -14
  41. package/src/mcp/tools/register-domain.ts +3 -12
  42. package/src/mcp/tools/register-feature.ts +22 -31
  43. package/src/mcp/tools/trace-vertical-slice.ts +8 -8
  44. package/src/types/index.ts +36 -5
@@ -1,5 +1,4 @@
1
1
  import type { ExtractedDependency, ExtractedSymbol, ParsedFileAST, Visibility } from "../../../types/index.ts";
2
- import { findPythonBlockEnd, calculatePythonNesting } from "../boundary-tracker.ts";
3
2
  import type { CodeExtractor } from "./base.ts";
4
3
 
5
4
  const PYTHON_STDLIB_AND_EXTERNALS = new Set([
@@ -47,59 +46,104 @@ const PYTHON_STDLIB_AND_EXTERNALS = new Set([
47
46
  "redis",
48
47
  ]);
49
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
+
50
72
  export class PythonExtractor implements CodeExtractor {
51
73
  public canHandle(filePath: string): boolean {
52
74
  return /\.py$/i.test(filePath);
53
75
  }
54
76
 
55
- public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
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 {
56
89
  const symbols: ExtractedSymbol[] = [];
57
90
  const dependencies: ExtractedDependency[] = [];
58
91
 
59
92
  const lines = content.split("\n");
93
+ const cleanLines = blankCommentsAndStrings(content, { hashComments: true }).split("\n");
60
94
 
61
95
  // 1. Extract Python Imports
62
96
  for (let i = 0; i < lines.length; i++) {
63
97
  const line = lines[i];
64
- const trimmed = line.trim();
98
+ const trimmed = cleanLines[i].trim();
99
+ const rawTrimmed = line.trim();
65
100
  const lineNum = i + 1;
66
101
 
67
- // Skip comments
68
- if (trimmed.startsWith("#")) continue;
102
+ if (!trimmed || trimmed.startsWith("#")) continue;
69
103
 
70
104
  // Pattern A: from <module> import <symbols>
71
105
  const fromMatch = /^from\s+([.\w]+)\s+import\s+([*\w\s,()]+)/.exec(trimmed);
72
106
  if (fromMatch) {
73
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 !== "*");
74
113
  const isRelative = modulePath.startsWith(".");
75
114
  const topPackage = isRelative ? "" : modulePath.split(".")[0];
76
115
  const isExternal = !isRelative && PYTHON_STDLIB_AND_EXTERNALS.has(topPackage);
77
116
 
78
117
  dependencies.push({
79
118
  target: modulePath,
80
- statement: trimmed,
119
+ statement: rawTrimmed,
81
120
  line_number: lineNum,
82
121
  is_external: isExternal,
122
+ ...(importedList.length > 0 ? { imported_symbols: importedList } : {}),
83
123
  });
84
124
  continue;
85
125
  }
86
126
 
87
127
  // Pattern B: import <module> [as <alias>]
88
- const importMatch = /^import\s+([.\w]+)(?:\s+as\s+\w+)?/.exec(trimmed);
128
+ const importMatch = /^import\s+([.\w]+)(?:\s+as\s+(\w+))?/.exec(trimmed);
89
129
  if (importMatch) {
90
130
  const modulePath = importMatch[1];
131
+ const alias = importMatch[2];
91
132
  const topPackage = modulePath.split(".")[0];
92
133
  const isExternal = PYTHON_STDLIB_AND_EXTERNALS.has(topPackage);
134
+ const shortName = alias || modulePath.split(".").pop() || modulePath;
93
135
 
94
136
  dependencies.push({
95
137
  target: modulePath,
96
- statement: trimmed,
138
+ statement: rawTrimmed,
97
139
  line_number: lineNum,
98
140
  is_external: isExternal,
141
+ imported_symbols: [shortName],
99
142
  });
100
143
  }
101
144
  }
102
145
 
146
+
103
147
  // 2. Extract Python Classes and Functions
104
148
  let currentClass: string | null = null;
105
149
  let currentClassIndent = -1;
@@ -205,8 +249,8 @@ export class PythonExtractor implements CodeExtractor {
205
249
  ? "protected"
206
250
  : "public";
207
251
 
208
- const endLine = findPythonBlockEnd(lines, i);
209
- const nestingDepth = calculatePythonNesting(lines, i, endLine - 1);
252
+ const endLine = findPythonBlockEnd(cleanLines, i);
253
+ const nestingDepth = calculatePythonNesting(cleanLines, i, endLine - 1);
210
254
  symbols.push({
211
255
  name: isMethod ? `${currentClass}::${funcName}` : funcName,
212
256
  kind: isMethod ? "method" : "function",
@@ -225,4 +269,192 @@ export class PythonExtractor implements CodeExtractor {
225
269
  dependencies,
226
270
  };
227
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
+ }
228
459
  }
460
+