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,66 +1,122 @@
1
- import type { ExtractedDependency, ExtractedSymbol, ParsedFileAST, Visibility } from "../../../types/index.ts";
2
- import { findBraceBlockEnd, calculateControlFlowNesting } from "../boundary-tracker.ts";
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";
3
15
  import type { CodeExtractor } from "./base.ts";
4
16
 
17
+ const PHP_CONTROL_FLOW_NODES = new Set([
18
+ "if_statement",
19
+ "for_statement",
20
+ "foreach_statement",
21
+ "while_statement",
22
+ "do_statement",
23
+ "switch_statement",
24
+ "try_statement",
25
+ "catch_clause",
26
+ "match_expression",
27
+ "conditional_expression",
28
+ ]);
29
+
30
+ const PHP_FUNCTION_BOUNDARY_NODES = new Set([
31
+ "function_definition",
32
+ "method_declaration",
33
+ "anonymous_function_creation_expression",
34
+ "arrow_function",
35
+ ]);
36
+
5
37
  export class PHPExtractor implements CodeExtractor {
6
38
  public canHandle(filePath: string): boolean {
7
39
  return /\.php$/i.test(filePath);
8
40
  }
9
41
 
10
- public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
42
+ public async extract(_filePath: string, content: string): Promise<ParsedFileAST> {
43
+ const astResult = await parseWithTreeSitter("php", content, (rootNode) =>
44
+ this.extractFromAst(rootNode, content)
45
+ );
46
+ if (astResult) {
47
+ return astResult;
48
+ }
49
+
50
+ return this.extractFallback(content);
51
+ }
52
+
53
+ private isExternalNamespace(cleaned: string): boolean {
54
+ return (
55
+ cleaned.startsWith("Illuminate\\") ||
56
+ cleaned.startsWith("Symfony\\") ||
57
+ cleaned.startsWith("Psr\\") ||
58
+ cleaned.startsWith("Composer\\")
59
+ );
60
+ }
61
+
62
+ private extractFallback(content: string): ParsedFileAST {
11
63
  const symbols: ExtractedSymbol[] = [];
12
64
  const dependencies: ExtractedDependency[] = [];
13
65
 
14
66
  const lines = content.split("\n");
67
+ const cleanLines = blankCommentsAndStrings(content, { hashComments: true }).split("\n");
15
68
 
16
69
  // 1. Extract PHP use / import statements
17
70
  const useRegex = /^\s*use\s+([^;]+);/;
18
71
  for (let i = 0; i < lines.length; i++) {
72
+ if (!cleanLines[i]?.trim()) continue;
19
73
  const line = lines[i];
20
- const match = useRegex.exec(line);
74
+ const match = useRegex.exec(cleanLines[i]);
21
75
  if (match) {
22
76
  const fullTarget = match[1].trim();
23
- // Remove 'function ' or 'const ' prefix if any
24
- const cleaned = fullTarget.replace(/^(function|const)\s+/, "");
25
- const isExternal =
26
- cleaned.startsWith("Illuminate\\") ||
27
- cleaned.startsWith("Symfony\\") ||
28
- cleaned.startsWith("Psr\\") ||
29
- cleaned.startsWith("Composer\\");
77
+ const cleaned = fullTarget.replace(/^(function|const)\s+/, "").replace(/\s+as\s+[A-Za-z0-9_]+$/i, "").trim();
78
+ const aliasMatch = /\bas\s+([A-Za-z0-9_]+)$/i.exec(fullTarget);
79
+ const shortName = cleaned.split("\\").pop() || cleaned;
80
+ const importedSymbols = Array.from(new Set([shortName, ...(aliasMatch ? [aliasMatch[1]] : [])])).filter(Boolean);
30
81
 
31
82
  dependencies.push({
32
83
  target: cleaned,
33
84
  statement: line.trim(),
34
85
  line_number: i + 1,
35
- is_external: isExternal,
86
+ is_external: this.isExternalNamespace(cleaned),
87
+ ...(importedSymbols.length > 0 ? { imported_symbols: importedSymbols } : {}),
36
88
  });
37
89
  }
38
90
  }
39
91
 
92
+
40
93
  // 2. Extract PHP Class / Interface / Trait / Enum
41
94
  let currentContainer: string | null = null;
42
95
  let currentContainerEndLine = 0;
43
96
 
44
- for (let i = 0; i < lines.length; i++) {
45
- const line = lines[i];
46
- const trimmed = line.trim();
97
+ for (let i = 0; i < cleanLines.length; i++) {
98
+ const trimmed = cleanLines[i].trim();
99
+ const rawTrimmed = lines[i].trim();
47
100
  const lineNum = i + 1;
48
101
 
102
+ if (!trimmed) continue;
103
+
49
104
  if (currentContainer && lineNum > currentContainerEndLine) {
50
105
  currentContainer = null;
51
106
  currentContainerEndLine = 0;
52
107
  }
53
108
 
54
- // Class / Interface / Trait / Enum header
55
- const classMatch = /^(?:abstract\s+|final\s+|readonly\s+)*(class|interface|trait|enum)\s+([A-Za-z0-9_]+)/.exec(trimmed);
109
+ const classMatch = /^(?:abstract\s+|final\s+|readonly\s+)*\b(class|interface|trait|enum)\s+([A-Za-z0-9_]+)\b/.exec(
110
+ trimmed
111
+ );
56
112
  if (classMatch) {
57
113
  currentContainer = classMatch[2];
58
- const endLine = findBraceBlockEnd(lines, i);
114
+ const endLine = findBraceBlockEnd(cleanLines, i);
59
115
  currentContainerEndLine = endLine;
60
116
  symbols.push({
61
117
  name: classMatch[2],
62
- kind: classMatch[1] as any,
63
- signature: trimmed.replace(/\{$/, "").trim(),
118
+ kind: classMatch[1] as SymbolKind,
119
+ signature: rawTrimmed.replace(/\{$/, "").trim(),
64
120
  visibility: "public",
65
121
  line_start: lineNum,
66
122
  line_end: endLine,
@@ -69,10 +125,8 @@ export class PHPExtractor implements CodeExtractor {
69
125
  continue;
70
126
  }
71
127
 
72
- // Methods or Properties in class/interface/trait/enum
73
128
  if (currentContainer) {
74
- // PHP Constant: const STATUS_ACTIVE = 'active';
75
- const constMatch = /^(?:(public|protected|private)\s+)?const\s+([A-Za-z0-9_]+)\s*=/.exec(trimmed);
129
+ const constMatch = /^(?:(public|protected|private)\s+)?\bconst\s+([A-Za-z0-9_]+)\s*=/.exec(trimmed);
76
130
  if (constMatch) {
77
131
  const vis = (constMatch[1] as Visibility) || "public";
78
132
  const constName = constMatch[2];
@@ -88,15 +142,17 @@ export class PHPExtractor implements CodeExtractor {
88
142
  continue;
89
143
  }
90
144
 
91
- // PHP Property: public ?string $foo = null; or private readonly Bar $bar;
92
- const propMatch = /^(?:(public|protected|private)\s+)?(?:readonly\s+)?(?:static\s+)?(?:\??[A-Za-z0-9_\\\\]+\s+)?\$([A-Za-z0-9_]+)(?:\s*=[^;]+)?;\s*$/.exec(trimmed);
145
+ const propMatch =
146
+ /^(?:(public|protected|private)\s+)?(?:readonly\s+)?(?:static\s+)?(?:\??[A-Za-z0-9_\\]+\s+)?\$([A-Za-z0-9_]+)(?:\s*=[^;]+)?;\s*$/.exec(
147
+ trimmed
148
+ );
93
149
  if (propMatch && !trimmed.includes("function ")) {
94
150
  const vis = (propMatch[1] as Visibility) || "public";
95
151
  const propName = `$${propMatch[2]}`;
96
152
  symbols.push({
97
153
  name: `${currentContainer}::${propName}`,
98
154
  kind: "property",
99
- signature: `${vis} ${trimmed.replace(/;\s*$/, "")}`,
155
+ signature: `${vis} ${rawTrimmed.replace(/;\s*$/, "")}`,
100
156
  visibility: vis,
101
157
  line_start: lineNum,
102
158
  line_end: lineNum,
@@ -105,13 +161,16 @@ export class PHPExtractor implements CodeExtractor {
105
161
  continue;
106
162
  }
107
163
 
108
- const methodMatch = /^(?:(public|protected|private)\s+)?(?:static\s+)?(?:final\s+)?function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(trimmed);
164
+ const methodMatch =
165
+ /^(?:(public|protected|private)\s+)?(?:static\s+)?(?:final\s+)?\bfunction\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(
166
+ trimmed
167
+ );
109
168
  if (methodMatch) {
110
169
  const vis = (methodMatch[1] as Visibility) || "public";
111
170
  const name = methodMatch[2];
112
171
  const params = methodMatch[3].trim();
113
172
  const retType = methodMatch[4] ? `: ${methodMatch[4].trim()}` : "";
114
- const endLine = findBraceBlockEnd(lines, i);
173
+ const endLine = findBraceBlockEnd(cleanLines, i);
115
174
  const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
116
175
  symbols.push({
117
176
  name: `${currentContainer}::${name}`,
@@ -127,13 +186,12 @@ export class PHPExtractor implements CodeExtractor {
127
186
  }
128
187
  }
129
188
 
130
- // Standalone Global Function in PHP
131
- const funcMatch = /^(?:final\s+)?function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(trimmed);
189
+ const funcMatch = /^(?:final\s+)?\bfunction\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(trimmed);
132
190
  if (funcMatch && !currentContainer) {
133
191
  const name = funcMatch[1];
134
192
  const params = funcMatch[2].trim();
135
193
  const retType = funcMatch[3] ? `: ${funcMatch[3].trim()}` : "";
136
- const endLine = findBraceBlockEnd(lines, i);
194
+ const endLine = findBraceBlockEnd(cleanLines, i);
137
195
  const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
138
196
  symbols.push({
139
197
  name,
@@ -150,4 +208,208 @@ export class PHPExtractor implements CodeExtractor {
150
208
 
151
209
  return { symbols, dependencies };
152
210
  }
211
+
212
+ private extractFromAst(rootNode: Parser.SyntaxNode, content: string): ParsedFileAST {
213
+ const symbols: ExtractedSymbol[] = [];
214
+ const dependencies: ExtractedDependency[] = [];
215
+ const lines = content.split("\n");
216
+
217
+ const walk = (node: Parser.SyntaxNode): void => {
218
+ if (node.type === "namespace_use_declaration") {
219
+ const rawStmt = (lines[node.startPosition.row] ?? node.text).trim();
220
+ const bodyMatch = /^\s*use\s+([^;]+);/i.exec(node.text.replace(/\r?\n/g, " "));
221
+ if (bodyMatch) {
222
+ const fullTarget = bodyMatch[1].trim();
223
+ const cleaned = fullTarget.replace(/^(function|const)\s+/i, "").replace(/\s+as\s+[A-Za-z0-9_]+$/i, "").trim();
224
+ const aliasMatch = /\bas\s+([A-Za-z0-9_]+)$/i.exec(fullTarget);
225
+ const shortName = cleaned.split("\\").pop() || cleaned;
226
+ const importedSymbols = Array.from(
227
+ new Set([shortName, ...(aliasMatch ? [aliasMatch[1]] : [])])
228
+ ).filter(Boolean);
229
+
230
+ dependencies.push({
231
+ target: cleaned,
232
+ statement: rawStmt,
233
+ line_number: node.startPosition.row + 1,
234
+ is_external: this.isExternalNamespace(cleaned),
235
+ ...(importedSymbols.length > 0 ? { imported_symbols: importedSymbols } : {}),
236
+ });
237
+ }
238
+ return;
239
+ }
240
+
241
+ if (
242
+ node.type === "class_declaration" ||
243
+ node.type === "interface_declaration" ||
244
+ node.type === "trait_declaration" ||
245
+ node.type === "enum_declaration"
246
+ ) {
247
+ const nameNode = node.childForFieldName("name") ?? node.namedChildren.find((c) => c.type === "name");
248
+ if (!nameNode) return;
249
+
250
+ const containerName = nameNode.text;
251
+ const kindMap: Record<string, SymbolKind> = {
252
+ class_declaration: "class",
253
+ interface_declaration: "interface",
254
+ trait_declaration: "trait",
255
+ enum_declaration: "enum",
256
+ };
257
+ const lineStart = node.startPosition.row + 1;
258
+ const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
259
+ const headerLine = (lines[node.startPosition.row] ?? "").trim().replace(/\{$/, "").trim();
260
+
261
+ symbols.push({
262
+ name: containerName,
263
+ kind: kindMap[node.type] ?? "class",
264
+ signature: headerLine || `${kindMap[node.type]} ${containerName}`,
265
+ visibility: "public",
266
+ line_start: lineStart,
267
+ line_end: lineEnd,
268
+ line_count: Math.max(1, lineEnd - lineStart + 1),
269
+ });
270
+
271
+ const bodyNode =
272
+ node.childForFieldName("body") ?? node.namedChildren.find((c) => c.type === "declaration_list" || c.type === "enum_declaration_list");
273
+ if (bodyNode) {
274
+ this.extractPhpContainerMembers(containerName, bodyNode, lines, symbols);
275
+ }
276
+ return;
277
+ }
278
+
279
+ if (node.type === "function_definition") {
280
+ const nameNode = node.childForFieldName("name") ?? node.namedChildren.find((c) => c.type === "name");
281
+ if (!nameNode) return;
282
+
283
+ const fnName = nameNode.text;
284
+ const paramsNode = node.childForFieldName("parameters") ?? node.namedChildren.find((c) => c.type === "formal_parameters");
285
+ const retNode = node.childForFieldName("return_type");
286
+ const paramsText = paramsNode ? paramsNode.text : "()";
287
+ const retText = retNode ? `: ${retNode.text.replace(/^:\s*/, "")}` : "";
288
+ const lineStart = node.startPosition.row + 1;
289
+ const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
290
+ const nestingDepth = calculateAstControlFlowNesting(
291
+ node,
292
+ PHP_CONTROL_FLOW_NODES,
293
+ PHP_FUNCTION_BOUNDARY_NODES
294
+ );
295
+
296
+ symbols.push({
297
+ name: fnName,
298
+ kind: "function",
299
+ signature: `function ${fnName}${paramsText}${retText}`,
300
+ visibility: "public",
301
+ line_start: lineStart,
302
+ line_end: lineEnd,
303
+ line_count: Math.max(1, lineEnd - lineStart + 1),
304
+ nesting_depth: nestingDepth,
305
+ });
306
+ return;
307
+ }
308
+
309
+ for (const child of node.namedChildren) {
310
+ walk(child);
311
+ }
312
+ };
313
+
314
+ walk(rootNode);
315
+ return { symbols, dependencies };
316
+ }
317
+
318
+ private extractPhpContainerMembers(
319
+ containerName: string,
320
+ bodyNode: Parser.SyntaxNode,
321
+ lines: string[],
322
+ symbols: ExtractedSymbol[]
323
+ ): void {
324
+ for (const member of bodyNode.namedChildren) {
325
+ const mStart = member.startPosition.row + 1;
326
+ const mEnd = Math.max(mStart, member.endPosition.row + 1);
327
+ const mCount = Math.max(1, mEnd - mStart + 1);
328
+ const vis = this.extractPhpVisibility(member);
329
+
330
+ if (member.type === "method_declaration") {
331
+ const nameNode = member.childForFieldName("name") ?? member.namedChildren.find((c) => c.type === "name");
332
+ if (!nameNode) continue;
333
+
334
+ const methodName = nameNode.text;
335
+ const paramsNode =
336
+ member.childForFieldName("parameters") ??
337
+ member.namedChildren.find((c) => c.type === "formal_parameters");
338
+ const retNode = member.childForFieldName("return_type");
339
+ const paramsText = paramsNode ? paramsNode.text : "()";
340
+ const retText = retNode ? `: ${retNode.text.replace(/^:\s*/, "")}` : "";
341
+ const nestingDepth = calculateAstControlFlowNesting(
342
+ member,
343
+ PHP_CONTROL_FLOW_NODES,
344
+ PHP_FUNCTION_BOUNDARY_NODES
345
+ );
346
+
347
+ symbols.push({
348
+ name: `${containerName}::${methodName}`,
349
+ kind: "method",
350
+ signature: `${vis} function ${methodName}${paramsText}${retText}`,
351
+ visibility: vis,
352
+ line_start: mStart,
353
+ line_end: mEnd,
354
+ line_count: mCount,
355
+ nesting_depth: nestingDepth,
356
+ });
357
+ continue;
358
+ }
359
+
360
+ if (member.type === "property_declaration") {
361
+ const propElem = member.namedChildren.find((c) => c.type === "property_element");
362
+ const varNode =
363
+ propElem?.namedChildren.find((c) => c.type === "variable_name") ??
364
+ member.namedChildren.find((c) => c.type === "variable_name");
365
+ if (!varNode) continue;
366
+
367
+ const propName = varNode.text.startsWith("$") ? varNode.text : `$${varNode.text}`;
368
+ const rawLine = (lines[member.startPosition.row] ?? member.text).trim().replace(/;\s*$/, "");
369
+
370
+ symbols.push({
371
+ name: `${containerName}::${propName}`,
372
+ kind: "property",
373
+ signature: rawLine.startsWith(vis) ? rawLine : `${vis} ${rawLine}`,
374
+ visibility: vis,
375
+ line_start: mStart,
376
+ line_end: mEnd,
377
+ line_count: mCount,
378
+ });
379
+ continue;
380
+ }
381
+
382
+ if (member.type === "const_declaration") {
383
+ const constElem = member.namedChildren.find((c) => c.type === "const_element");
384
+ const nameNode =
385
+ constElem?.namedChildren.find((c) => c.type === "name") ??
386
+ member.namedChildren.find((c) => c.type === "name");
387
+ if (!nameNode) continue;
388
+
389
+ const constName = nameNode.text;
390
+ symbols.push({
391
+ name: `${containerName}::${constName}`,
392
+ kind: "property",
393
+ signature: `${vis} const ${constName}`,
394
+ visibility: vis,
395
+ line_start: mStart,
396
+ line_end: mEnd,
397
+ line_count: mCount,
398
+ });
399
+ }
400
+ }
401
+ }
402
+
403
+ private extractPhpVisibility(node: Parser.SyntaxNode): Visibility {
404
+ for (const child of node.children) {
405
+ if (child.type === "visibility_modifier") {
406
+ const t = child.text.trim().toLowerCase();
407
+ if (t === "private" || t === "protected" || t === "public") {
408
+ return t;
409
+ }
410
+ }
411
+ }
412
+ return "public";
413
+ }
153
414
  }
415
+