septum 0.1.5 → 0.1.7

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 (51) hide show
  1. package/README.md +23 -3
  2. package/package.json +5 -5
  3. package/src/cli/commands/check.ts +32 -122
  4. package/src/cli/commands/hook.ts +45 -12
  5. package/src/cli/commands/init.ts +6 -4
  6. package/src/cli/commands/query.ts +45 -5
  7. package/src/cli/commands/slice.ts +4 -2
  8. package/src/cli/index.ts +17 -6
  9. package/src/cli/templates/hooks.ts +6 -6
  10. package/src/cli/templates/rules.ts +35 -23
  11. package/src/cli/templates/skill.ts +47 -51
  12. package/src/core/boundary/evaluator.ts +217 -24
  13. package/src/core/boundary/staged-auditor.ts +47 -0
  14. package/src/core/config/loader.ts +29 -12
  15. package/src/core/database/repositories/dependency.repository.ts +252 -66
  16. package/src/core/database/repositories/domain.repository.ts +96 -39
  17. package/src/core/database/repositories/file.repository.ts +20 -19
  18. package/src/core/database/repositories/symbol.repository.ts +115 -5
  19. package/src/core/database/repository.ts +19 -2
  20. package/src/core/database/schema.ts +3 -2
  21. package/src/core/ingestion/pipeline.ts +96 -60
  22. package/src/core/parser/boundary-tracker.ts +145 -0
  23. package/src/core/parser/extractors/frontend.ts +44 -90
  24. package/src/core/parser/extractors/go.ts +200 -21
  25. package/src/core/parser/extractors/laravel-semantic.ts +18 -12
  26. package/src/core/parser/extractors/mcp-cli-semantic.ts +173 -44
  27. package/src/core/parser/extractors/nestjs-semantic.ts +20 -5
  28. package/src/core/parser/extractors/php.ts +293 -31
  29. package/src/core/parser/extractors/python.ts +242 -10
  30. package/src/core/parser/extractors/typescript.ts +604 -117
  31. package/src/core/parser/tree-sitter.ts +6 -1
  32. package/src/core/parser/wasm-assets.ts +17 -0
  33. package/src/core/parser/wasm-loader.ts +274 -0
  34. package/src/core/resolver/call-graph-tracer.ts +49 -17
  35. package/src/core/resolver/module-resolver.ts +88 -26
  36. package/src/core/resolver/vertical-slice-tracer.ts +34 -11
  37. package/src/core/session/session-manager.ts +55 -39
  38. package/src/core/topology/environment-detector.ts +109 -6
  39. package/src/mcp/schemas.ts +146 -29
  40. package/src/mcp/server.ts +177 -114
  41. package/src/mcp/tools/check-boundary.ts +159 -41
  42. package/src/mcp/tools/get-domain-catalog.ts +30 -15
  43. package/src/mcp/tools/get-environment-topology.ts +3 -6
  44. package/src/mcp/tools/get-feature-context.ts +35 -20
  45. package/src/mcp/tools/get-symbol-hotspots.ts +4 -10
  46. package/src/mcp/tools/locate-symbol.ts +37 -16
  47. package/src/mcp/tools/register-domain.ts +3 -12
  48. package/src/mcp/tools/register-feature.ts +25 -32
  49. package/src/mcp/tools/trace-vertical-slice.ts +8 -8
  50. package/src/types/index.ts +56 -6
  51. package/src/types/wasm.d.ts +4 -0
@@ -39,16 +39,46 @@ const GO_STDLIB_PREFIXES = new Set([
39
39
  "unsafe",
40
40
  ]);
41
41
 
42
+ import type Parser from "web-tree-sitter";
43
+ import { blankCommentsAndStrings } from "../boundary-tracker.ts";
44
+ import { calculateAstControlFlowNesting, parseWithTreeSitter } from "../wasm-loader.ts";
45
+
46
+ const GO_CONTROL_FLOW_NODES = new Set([
47
+ "if_statement",
48
+ "for_statement",
49
+ "expression_switch_statement",
50
+ "type_switch_statement",
51
+ "select_statement",
52
+ ]);
53
+
54
+ const GO_FUNCTION_BOUNDARY_NODES = new Set([
55
+ "function_declaration",
56
+ "method_declaration",
57
+ "func_literal",
58
+ ]);
59
+
42
60
  export class GoExtractor implements CodeExtractor {
43
61
  public canHandle(filePath: string): boolean {
44
62
  return /\.go$/i.test(filePath);
45
63
  }
46
64
 
47
65
  public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
66
+ const astResult = await parseWithTreeSitter("go", content, (rootNode) =>
67
+ this.extractFromAst(rootNode, filePath, content)
68
+ );
69
+ if (astResult) {
70
+ return { ...astResult, status: "parsed" };
71
+ }
72
+
73
+ return { ...this.extractFallback(filePath, content), status: "degraded" };
74
+ }
75
+
76
+ private extractFallback(filePath: string, content: string): ParsedFileAST {
48
77
  const symbols: ExtractedSymbol[] = [];
49
78
  const dependencies: ExtractedDependency[] = [];
50
79
 
51
80
  const lines = content.split("\n");
81
+ const cleanLines = blankCommentsAndStrings(content).split("\n");
52
82
 
53
83
  // 1. Extract Go Imports
54
84
  let inImportBlock = false;
@@ -56,69 +86,74 @@ export class GoExtractor implements CodeExtractor {
56
86
  for (let i = 0; i < lines.length; i++) {
57
87
  const line = lines[i];
58
88
  const trimmed = line.trim();
89
+ const cleanTrimmed = cleanLines[i].trim();
59
90
  const lineNum = i + 1;
60
91
 
61
- if (trimmed.startsWith("//")) continue;
92
+ if (!cleanTrimmed || trimmed.startsWith("//")) continue;
62
93
 
63
- if (trimmed === "import (") {
94
+ if (cleanTrimmed.startsWith("import (")) {
64
95
  inImportBlock = true;
65
96
  continue;
66
97
  }
67
98
 
68
99
  if (inImportBlock) {
69
- if (trimmed === ")") {
100
+ if (cleanTrimmed === ")") {
70
101
  inImportBlock = false;
71
102
  continue;
72
103
  }
73
104
 
74
- const blockMatch = /^(?:[\w.]+\s+)?["']([^"']+)["']/.exec(trimmed);
105
+ const blockMatch = /^(?:([\w.]+)\s+)?["'`]([^"'`]+)["'`]/.exec(trimmed);
75
106
  if (blockMatch) {
76
- const importPath = blockMatch[1];
107
+ const alias = blockMatch[1];
108
+ const importPath = blockMatch[2];
77
109
  const isExternal = this.isExternalImport(importPath, filePath);
110
+ const pkgSymbol = alias && alias !== "." && alias !== "_" ? alias : importPath.split("/").pop() || importPath;
78
111
  dependencies.push({
79
112
  target: importPath,
80
113
  statement: trimmed,
81
114
  line_number: lineNum,
82
115
  is_external: isExternal,
116
+ imported_symbols: [pkgSymbol],
83
117
  });
84
118
  }
85
119
  continue;
86
120
  }
87
121
 
88
- // Single line import: import "fmt" or import alias "pkg"
89
- const singleMatch = /^import\s+(?:[\w.]+\s+)?["']([^"']+)["']/.exec(trimmed);
122
+ const singleMatch = /^import\s+(?:([\w.]+)\s+)?["'`]([^"'`]+)["'`]/.exec(trimmed);
90
123
  if (singleMatch) {
91
- const importPath = singleMatch[1];
124
+ const alias = singleMatch[1];
125
+ const importPath = singleMatch[2];
92
126
  const isExternal = this.isExternalImport(importPath, filePath);
127
+ const pkgSymbol = alias && alias !== "." && alias !== "_" ? alias : importPath.split("/").pop() || importPath;
93
128
  dependencies.push({
94
129
  target: importPath,
95
130
  statement: trimmed,
96
131
  line_number: lineNum,
97
132
  is_external: isExternal,
133
+ imported_symbols: [pkgSymbol],
98
134
  });
99
135
  }
100
136
  }
101
137
 
102
138
  // 2. Extract Go Types, Structs, Interfaces, and Functions
103
- for (let i = 0; i < lines.length; i++) {
104
- const line = lines[i];
105
- const trimmed = line.trim();
139
+ for (let i = 0; i < cleanLines.length; i++) {
140
+ const trimmed = cleanLines[i].trim();
141
+ const rawTrimmed = lines[i].trim();
106
142
  const lineNum = i + 1;
107
143
 
108
- if (trimmed.startsWith("//")) continue;
144
+ if (!trimmed) continue;
109
145
 
110
- // Struct or Interface: type User struct / type Reader interface
111
146
  const typeMatch = /^type\s+([A-Za-z0-9_]+)\s+(struct|interface)\b/.exec(trimmed);
112
147
  if (typeMatch) {
113
148
  const typeName = typeMatch[1];
114
149
  const kind = typeMatch[2] === "interface" ? "interface" : "class";
115
150
  const visibility: Visibility = /^[A-Z]/.test(typeName) ? "public" : "private";
116
- const endLine = findBraceBlockEnd(lines, i);
151
+ const endLine = findBraceBlockEnd(cleanLines, i);
117
152
 
118
153
  symbols.push({
119
154
  name: typeName,
120
155
  kind,
121
- signature: trimmed.replace(/\{$/, "").trim(),
156
+ signature: rawTrimmed.replace(/\{$/, "").trim(),
122
157
  visibility,
123
158
  line_start: lineNum,
124
159
  line_end: endLine,
@@ -127,18 +162,17 @@ export class GoExtractor implements CodeExtractor {
127
162
  continue;
128
163
  }
129
164
 
130
- // Method with receiver: func (r *Repo) FindUser(...) (...)
131
165
  const methodMatch = /^func\s*\([^)]+\)\s*([A-Za-z0-9_]+)\s*\([^)]*\)/.exec(trimmed);
132
166
  if (methodMatch) {
133
167
  const methodName = methodMatch[1];
134
168
  const visibility: Visibility = /^[A-Z]/.test(methodName) ? "public" : "private";
135
- const endLine = findBraceBlockEnd(lines, i);
169
+ const endLine = findBraceBlockEnd(cleanLines, i);
136
170
  const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
137
171
 
138
172
  symbols.push({
139
173
  name: methodName,
140
174
  kind: "method",
141
- signature: trimmed.replace(/\{$/, "").trim(),
175
+ signature: rawTrimmed.replace(/\{$/, "").trim(),
142
176
  visibility,
143
177
  line_start: lineNum,
144
178
  line_end: endLine,
@@ -148,18 +182,17 @@ export class GoExtractor implements CodeExtractor {
148
182
  continue;
149
183
  }
150
184
 
151
- // Function: func ProcessPayment(...) (...)
152
185
  const funcMatch = /^func\s+([A-Za-z0-9_]+)\s*\([^)]*\)/.exec(trimmed);
153
186
  if (funcMatch) {
154
187
  const funcName = funcMatch[1];
155
188
  const visibility: Visibility = /^[A-Z]/.test(funcName) ? "public" : "private";
156
- const endLine = findBraceBlockEnd(lines, i);
189
+ const endLine = findBraceBlockEnd(cleanLines, i);
157
190
  const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
158
191
 
159
192
  symbols.push({
160
193
  name: funcName,
161
194
  kind: "function",
162
- signature: trimmed.replace(/\{$/, "").trim(),
195
+ signature: rawTrimmed.replace(/\{$/, "").trim(),
163
196
  visibility,
164
197
  line_start: lineNum,
165
198
  line_end: endLine,
@@ -175,6 +208,7 @@ export class GoExtractor implements CodeExtractor {
175
208
  };
176
209
  }
177
210
 
211
+
178
212
  private isExternalImport(importPath: string, filePath?: string): boolean {
179
213
  if (this.isGoStdlib(importPath)) {
180
214
  return true;
@@ -226,4 +260,149 @@ export class GoExtractor implements CodeExtractor {
226
260
  const rootSegment = importPath.split("/")[0];
227
261
  return GO_STDLIB_PREFIXES.has(rootSegment) || !importPath.includes(".");
228
262
  }
263
+
264
+ private extractFromAst(
265
+ rootNode: Parser.SyntaxNode,
266
+ filePath: string,
267
+ content: string
268
+ ): ParsedFileAST {
269
+ const symbols: ExtractedSymbol[] = [];
270
+ const dependencies: ExtractedDependency[] = [];
271
+ const lines = content.split("\n");
272
+
273
+ const addImportSpec = (specNode: Parser.SyntaxNode): void => {
274
+ const pathNode =
275
+ specNode.childForFieldName("path") ??
276
+ specNode.namedChildren.find(
277
+ (c) => c.type === "interpreted_string_literal" || c.type === "raw_string_literal"
278
+ );
279
+ if (!pathNode) return;
280
+
281
+ const importPath = pathNode.text.replace(/^["'`]|["'`]$/g, "").trim();
282
+ if (!importPath) return;
283
+
284
+ const nameNode =
285
+ specNode.childForFieldName("name") ??
286
+ specNode.namedChildren.find((c) => c.type === "package_identifier" || c.type === "dot" || c.type === "blank_identifier");
287
+ const alias = nameNode?.text?.trim();
288
+ const pkgSymbol =
289
+ alias && alias !== "." && alias !== "_" ? alias : importPath.split("/").pop() || importPath;
290
+
291
+ dependencies.push({
292
+ target: importPath,
293
+ statement: (lines[specNode.startPosition.row] ?? specNode.text).trim(),
294
+ line_number: specNode.startPosition.row + 1,
295
+ is_external: this.isExternalImport(importPath, filePath),
296
+ imported_symbols: [pkgSymbol],
297
+ });
298
+ };
299
+
300
+ for (const node of rootNode.namedChildren) {
301
+ if (node.type === "import_declaration") {
302
+ for (const child of node.namedChildren) {
303
+ if (child.type === "import_spec") {
304
+ addImportSpec(child);
305
+ } else if (child.type === "import_spec_list") {
306
+ for (const spec of child.namedChildren) {
307
+ if (spec.type === "import_spec") {
308
+ addImportSpec(spec);
309
+ }
310
+ }
311
+ }
312
+ }
313
+ continue;
314
+ }
315
+
316
+ if (node.type === "type_declaration") {
317
+ for (const spec of node.namedChildren) {
318
+ if (spec.type !== "type_spec" && spec.type !== "type_alias") continue;
319
+ const nameNode =
320
+ spec.childForFieldName("name") ??
321
+ spec.namedChildren.find((c) => c.type === "type_identifier");
322
+ if (!nameNode) continue;
323
+
324
+ const typeName = nameNode.text;
325
+ const typeVal = spec.childForFieldName("type") ?? spec.namedChildren[1];
326
+ const kind = typeVal?.type === "interface_type" ? "interface" : "class";
327
+ const visibility: Visibility = /^[A-Z]/.test(typeName) ? "public" : "private";
328
+ const lineStart = spec.startPosition.row + 1;
329
+ const lineEnd = Math.max(lineStart, spec.endPosition.row + 1);
330
+ const headerLine = (lines[spec.startPosition.row] ?? "").trim().replace(/\{$/, "").trim();
331
+
332
+ symbols.push({
333
+ name: typeName,
334
+ kind,
335
+ signature: headerLine || `type ${typeName}`,
336
+ visibility,
337
+ line_start: lineStart,
338
+ line_end: lineEnd,
339
+ line_count: Math.max(1, lineEnd - lineStart + 1),
340
+ });
341
+ }
342
+ continue;
343
+ }
344
+
345
+ if (node.type === "method_declaration") {
346
+ const nameNode =
347
+ node.childForFieldName("name") ??
348
+ node.namedChildren.find((c) => c.type === "field_identifier" || c.type === "identifier");
349
+ if (!nameNode) continue;
350
+
351
+ const methodName = nameNode.text;
352
+ const visibility: Visibility = /^[A-Z]/.test(methodName) ? "public" : "private";
353
+ const lineStart = node.startPosition.row + 1;
354
+ const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
355
+ const headerLine = (lines[node.startPosition.row] ?? "").trim().replace(/\{$/, "").trim();
356
+ const nestingDepth = calculateAstControlFlowNesting(
357
+ node,
358
+ GO_CONTROL_FLOW_NODES,
359
+ GO_FUNCTION_BOUNDARY_NODES
360
+ );
361
+
362
+ symbols.push({
363
+ name: methodName,
364
+ kind: "method",
365
+ signature: headerLine || `func ${methodName}()`,
366
+ visibility,
367
+ line_start: lineStart,
368
+ line_end: lineEnd,
369
+ line_count: Math.max(1, lineEnd - lineStart + 1),
370
+ nesting_depth: nestingDepth,
371
+ });
372
+ continue;
373
+ }
374
+
375
+ if (node.type === "function_declaration") {
376
+ const nameNode =
377
+ node.childForFieldName("name") ??
378
+ node.namedChildren.find((c) => c.type === "identifier");
379
+ if (!nameNode) continue;
380
+
381
+ const funcName = nameNode.text;
382
+ const visibility: Visibility = /^[A-Z]/.test(funcName) ? "public" : "private";
383
+ const lineStart = node.startPosition.row + 1;
384
+ const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
385
+ const headerLine = (lines[node.startPosition.row] ?? "").trim().replace(/\{$/, "").trim();
386
+ const nestingDepth = calculateAstControlFlowNesting(
387
+ node,
388
+ GO_CONTROL_FLOW_NODES,
389
+ GO_FUNCTION_BOUNDARY_NODES
390
+ );
391
+
392
+ symbols.push({
393
+ name: funcName,
394
+ kind: "function",
395
+ signature: headerLine || `func ${funcName}()`,
396
+ visibility,
397
+ line_start: lineStart,
398
+ line_end: lineEnd,
399
+ line_count: Math.max(1, lineEnd - lineStart + 1),
400
+ nesting_depth: nestingDepth,
401
+ });
402
+ }
403
+ }
404
+
405
+ return { symbols, dependencies };
406
+ }
229
407
  }
408
+
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { ExecutionChainNode, VerticalSliceRecord } from "../../../types/index.ts";
4
4
  import { BehavioralAnalyzer } from "../behavioral-analyzer.ts";
5
+ import { blankCommentsAndStrings } from "../boundary-tracker.ts";
5
6
  import { SchemaProviderRegistry } from "../schema/schema-provider-registry.ts";
6
7
  import type {
7
8
  SemanticSliceExtractor,
@@ -312,6 +313,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
312
313
 
313
314
  // 2. Parse routes with group context tracking (prefix, controller, name prefix)
314
315
  const lines = content.split(/\r?\n/);
316
+ const cleanLines = blankCommentsAndStrings(content, { hashComments: true }).split(/\r?\n/);
315
317
  interface GroupContext {
316
318
  prefix: string;
317
319
  controller: string;
@@ -326,10 +328,14 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
326
328
 
327
329
  for (let i = 0; i < lines.length; i++) {
328
330
  const line = lines[i];
331
+ const cleanLine = cleanLines[i] ?? "";
332
+ const cleanTrimmed = cleanLine.trim();
329
333
  const trimmed = line.trim();
330
334
 
335
+ if (!cleanTrimmed) continue;
336
+
331
337
  // Check group opens: Route::...group(
332
- if (trimmed.includes("Route::") && trimmed.includes("group(") && trimmed.includes("function")) {
338
+ if (cleanTrimmed.includes("Route::") && cleanTrimmed.includes("group(") && cleanTrimmed.includes("function")) {
333
339
  const prefixMatch = /prefix\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(trimmed);
334
340
  const controllerMatch = /controller\s*\(\s*([\\A-Za-z0-9_]+)::class\s*\)/.exec(trimmed);
335
341
  const nameMatch = /name\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(trimmed);
@@ -351,8 +357,8 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
351
357
  });
352
358
  }
353
359
 
354
- // Track brace depth
355
- for (const char of line) {
360
+ // Track brace depth on string/comment-stripped line so '/orders/{id}' never corrupts braceDepth
361
+ for (const char of cleanLine) {
356
362
  if (char === "{") currentBraceDepth++;
357
363
  if (char === "}") {
358
364
  currentBraceDepth--;
@@ -366,14 +372,14 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
366
372
  }
367
373
 
368
374
  // Route statement accumulation across lines
369
- if (!inStatement && trimmed.startsWith("Route::") && !trimmed.includes("Route::resource") && !trimmed.includes("Route::group")) {
375
+ if (!inStatement && cleanTrimmed.startsWith("Route::") && !cleanTrimmed.includes("Route::resource") && !cleanTrimmed.includes("Route::group") && !(cleanTrimmed.includes("group(") && cleanTrimmed.includes("function"))) {
370
376
  statementBuffer = trimmed;
371
377
  inStatement = true;
372
378
  } else if (inStatement) {
373
379
  statementBuffer += " " + trimmed;
374
380
  }
375
381
 
376
- if (inStatement && statementBuffer.includes(";")) {
382
+ if (inStatement && cleanLine.includes(";")) {
377
383
  inStatement = false;
378
384
  const stmt = statementBuffer;
379
385
  statementBuffer = "";
@@ -519,16 +525,17 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
519
525
  }
520
526
 
521
527
  private extractActionBody(controllerCode: string, actionName: string): string {
522
- const fnRegex = new RegExp(`public\\s+function\\s+${actionName}\\s*\\([^{]*\\)\\s*\\{`, "i");
523
- const match = fnRegex.exec(controllerCode);
528
+ const cleanCode = blankCommentsAndStrings(controllerCode, { hashComments: true });
529
+ const fnRegex = new RegExp(`(?:public\\s+)?function\\s+${actionName}\\s*\\([^{]*\\)\\s*\\{`, "i");
530
+ const match = fnRegex.exec(cleanCode);
524
531
  if (!match) return "";
525
532
 
526
533
  const startIndex = match.index + match[0].length;
527
534
  let braceDepth = 1;
528
535
  let endIndex = startIndex;
529
536
 
530
- while (braceDepth > 0 && endIndex < controllerCode.length) {
531
- const char = controllerCode[endIndex];
537
+ while (braceDepth > 0 && endIndex < cleanCode.length) {
538
+ const char = cleanCode[endIndex];
532
539
  if (char === "{") braceDepth++;
533
540
  else if (char === "}") braceDepth--;
534
541
  endIndex++;
@@ -750,10 +757,9 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
750
757
 
751
758
  private extractValidationRules(requestCode: string): Record<string, string> {
752
759
  const rules: Record<string, string> = {};
753
- const rulesMatch = requestCode.match(/function\s+rules\s*\(\s*\)[^{]*\{([^}]*)\}/i);
754
- if (!rulesMatch) return rules;
760
+ const body = this.extractActionBody(requestCode, "rules");
761
+ if (!body) return rules;
755
762
 
756
- const body = rulesMatch[1];
757
763
  // Match 'status' => 'in:pending,paid' or 'status' => ['required', 'string']
758
764
  const rulePairRegex = /['"]([A-Za-z0-9_]+)['"]\s*=>\s*(?:['"]([^'"]+)['"]|\[([^\]]+)\])/g;
759
765
  let rm;