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
@@ -30,6 +30,8 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
30
30
 
31
31
  public extractSlices(projectRoot: string, domainId?: number): VerticalSliceCandidate[] {
32
32
  const slices: VerticalSliceCandidate[] = [];
33
+ const { toolPrefix, cliBinName } = this.resolveProjectNaming(projectRoot);
34
+ const registeredTools = this.scanRegisteredMcpToolNames(projectRoot);
33
35
 
34
36
  // 1. Extract MCP Tools
35
37
  const mcpToolsDir = path.join(projectRoot, "src/mcp/tools");
@@ -41,30 +43,46 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
41
43
 
42
44
  for (const file of toolFiles) {
43
45
  const fullPath = path.join(mcpToolsDir, file);
44
- const relPath = path.relative(projectRoot, fullPath);
46
+ const relPath = path.relative(projectRoot, fullPath).replace(/\\/g, "/");
45
47
  const content = fs.readFileSync(fullPath, "utf-8");
46
- const baseName = path.basename(file, path.extname(file)); // e.g. 'get-domain-catalog'
47
- const toolIdentifier = `septum_${baseName.replace(/-/g, "_")}`;
48
+ const baseName = path.basename(file, path.extname(file));
49
+ const snakeBase = baseName.replace(/-/g, "_");
50
+ const toolIdentifier =
51
+ registeredTools.get(snakeBase) || `${toolPrefix}_${snakeBase}`;
48
52
 
49
- // Detect handler function
50
- const handlerMatch = content.match(/export\s+async?\s+function\s+([a-zA-Z0-9_]+)/);
51
- const actionName = handlerMatch ? handlerMatch[1] : `handle_${baseName}`;
53
+ // Detect handler function (both sync and async) and its 1-indexed line number
54
+ const handlerMatch = /export\s+(?:async\s+)?function\s+([a-zA-Z0-9_]+)/.exec(content);
55
+ const classMatch = /export\s+class\s+([A-Za-z0-9_]+)/.exec(content);
56
+ const actionName = handlerMatch ? handlerMatch[1] : `handle_${snakeBase}`;
57
+ const controllerClass = classMatch ? classMatch[1] : baseName;
58
+ const handlerLine =
59
+ handlerMatch && handlerMatch.index !== undefined
60
+ ? content.slice(0, handlerMatch.index).split("\n").length
61
+ : 1;
52
62
 
53
- // Detect args interface
54
- const argsMatch = content.match(/export\s+interface\s+([a-zA-Z0-9_]+Args)/);
63
+ // Detect args interface and its line number
64
+ const argsMatch = /(?:export\s+)?interface\s+([a-zA-Z0-9_]+Args)\b/.exec(content);
55
65
  const formRequestClass = argsMatch ? argsMatch[1] : null;
66
+ const argsLine =
67
+ argsMatch && argsMatch.index !== undefined
68
+ ? content.slice(0, argsMatch.index).split("\n").length
69
+ : undefined;
56
70
 
57
- // Detect model/service dependency (e.g. SeptumRepository, ConfigLoader)
58
- let modelClass: string | null = null;
59
- if (content.includes("SeptumRepository")) modelClass = "SeptumRepository";
60
- else if (content.includes("SessionManager")) modelClass = "SessionManager";
61
- else if (content.includes("ConfigLoader")) modelClass = "ConfigLoader";
71
+ // Detect service/repository dependency and its actual file path
72
+ const serviceDep = this.resolveServiceDependency(projectRoot, fullPath, content);
62
73
 
63
- const mcpChain: Array<{ stage: string; symbol: string; file?: string; line?: number; description?: string }> = [
74
+ const mcpChain: Array<{
75
+ stage: string;
76
+ symbol: string;
77
+ file?: string;
78
+ line?: number;
79
+ description?: string;
80
+ }> = [
64
81
  {
65
82
  stage: "ingress",
66
83
  symbol: `tool:${toolIdentifier}`,
67
84
  file: relPath,
85
+ line: handlerLine,
68
86
  description: `MCP Tool Endpoint: ${toolIdentifier}`,
69
87
  },
70
88
  ];
@@ -74,23 +92,24 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
74
92
  stage: "validation",
75
93
  symbol: formRequestClass,
76
94
  file: relPath,
95
+ ...(argsLine ? { line: argsLine } : {}),
77
96
  description: "MCP Argument Schema",
78
97
  });
79
98
  }
80
99
 
81
100
  mcpChain.push({
82
101
  stage: "controller",
83
- symbol: `McpToolHandler::${actionName}`,
102
+ symbol: classMatch ? `${controllerClass}::${actionName}` : actionName,
84
103
  file: relPath,
85
- line: 1,
86
- description: `Tool Handler Execution`,
104
+ line: handlerLine,
105
+ description: "Tool Handler Execution",
87
106
  });
88
107
 
89
- if (modelClass) {
108
+ if (serviceDep) {
90
109
  mcpChain.push({
91
110
  stage: "service",
92
- symbol: modelClass,
93
- file: "src/core/database/repository.ts",
111
+ symbol: serviceDep.symbol,
112
+ ...(serviceDep.file ? { file: serviceDep.file } : {}),
94
113
  description: "Core Engine Service",
95
114
  });
96
115
  }
@@ -101,10 +120,10 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
101
120
  http_method: "MCP",
102
121
  route_uri: `tool:${toolIdentifier}`,
103
122
  route_name: toolIdentifier,
104
- controller_class: "McpToolHandler",
123
+ controller_class: controllerClass,
105
124
  action_name: actionName,
106
125
  controller_file: relPath,
107
- controller_line: 1,
126
+ controller_line: handlerLine,
108
127
  architecture_style: "rpc",
109
128
  entry_kind: "mcp_tool",
110
129
  execution_chain_json: JSON.stringify(mcpChain),
@@ -123,30 +142,44 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
123
142
 
124
143
  for (const file of commandFiles) {
125
144
  const fullPath = path.join(cliCommandsDir, file);
126
- const relPath = path.relative(projectRoot, fullPath);
145
+ const relPath = path.relative(projectRoot, fullPath).replace(/\\/g, "/");
127
146
  const content = fs.readFileSync(fullPath, "utf-8");
128
- const commandName = path.basename(file, path.extname(file)); // e.g. 'ingest', 'check'
147
+ const commandName = path.basename(file, path.extname(file));
129
148
 
130
- const handlerMatch = content.match(/export\s+async?\s+function\s+([a-zA-Z0-9_]+Command)/);
131
- const actionName = handlerMatch ? handlerMatch[1] : `handle${commandName.charAt(0).toUpperCase() + commandName.slice(1)}Command`;
149
+ const handlerMatch = /export\s+(?:async\s+)?function\s+([a-zA-Z0-9_]+)/.exec(content);
150
+ const classMatch = /export\s+class\s+([A-Za-z0-9_]+)/.exec(content);
151
+ const actionName = handlerMatch
152
+ ? handlerMatch[1]
153
+ : `handle${commandName.charAt(0).toUpperCase() + commandName.slice(1)}Command`;
154
+ const controllerClass = classMatch ? classMatch[1] : commandName;
155
+ const handlerLine =
156
+ handlerMatch && handlerMatch.index !== undefined
157
+ ? content.slice(0, handlerMatch.index).split("\n").length
158
+ : 1;
132
159
 
133
160
  // Detect options / args
134
- const optionsMatch = content.match(/interface\s+([a-zA-Z0-9_]+Options)/);
161
+ const optionsMatch = /(?:export\s+)?interface\s+([a-zA-Z0-9_]+(?:Options|Args))\b/.exec(content);
135
162
  const formRequestClass = optionsMatch ? optionsMatch[1] : null;
163
+ const optionsLine =
164
+ optionsMatch && optionsMatch.index !== undefined
165
+ ? content.slice(0, optionsMatch.index).split("\n").length
166
+ : undefined;
136
167
 
137
- // Detect Core Service used
138
- let modelClass: string | null = null;
139
- if (content.includes("IngestionPipeline")) modelClass = "IngestionPipeline";
140
- else if (content.includes("BoundaryEvaluator")) modelClass = "BoundaryEvaluator";
141
- else if (content.includes("VerticalSliceTracer")) modelClass = "VerticalSliceTracer";
142
- else if (content.includes("SeptumRepository")) modelClass = "SeptumRepository";
168
+ const serviceDep = this.resolveServiceDependency(projectRoot, fullPath, content);
143
169
 
144
- const cliChain: Array<{ stage: string; symbol: string; file?: string; line?: number; description?: string }> = [
170
+ const cliChain: Array<{
171
+ stage: string;
172
+ symbol: string;
173
+ file?: string;
174
+ line?: number;
175
+ description?: string;
176
+ }> = [
145
177
  {
146
178
  stage: "ingress",
147
- symbol: `septum ${commandName}`,
179
+ symbol: `${cliBinName} ${commandName}`,
148
180
  file: relPath,
149
- description: `CLI Command Entrypoint: septum ${commandName}`,
181
+ line: handlerLine,
182
+ description: `CLI Command Entrypoint: ${cliBinName} ${commandName}`,
150
183
  },
151
184
  ];
152
185
 
@@ -155,23 +188,24 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
155
188
  stage: "validation",
156
189
  symbol: formRequestClass,
157
190
  file: relPath,
191
+ ...(optionsLine ? { line: optionsLine } : {}),
158
192
  description: "CLI Options Interface",
159
193
  });
160
194
  }
161
195
 
162
196
  cliChain.push({
163
197
  stage: "controller",
164
- symbol: `CLICommandHandler::${actionName}`,
198
+ symbol: classMatch ? `${controllerClass}::${actionName}` : actionName,
165
199
  file: relPath,
166
- line: 1,
200
+ line: handlerLine,
167
201
  description: "Command Execution Handler",
168
202
  });
169
203
 
170
- if (modelClass) {
204
+ if (serviceDep) {
171
205
  cliChain.push({
172
206
  stage: "service",
173
- symbol: modelClass,
174
- file: "src/core/",
207
+ symbol: serviceDep.symbol,
208
+ ...(serviceDep.file ? { file: serviceDep.file } : {}),
175
209
  description: "Core Engine Service",
176
210
  });
177
211
  }
@@ -181,11 +215,11 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
181
215
  feature_key: `cli.${commandName}`,
182
216
  http_method: "CLI",
183
217
  route_uri: `command:${commandName}`,
184
- route_name: `septum ${commandName}`,
185
- controller_class: "CLICommandHandler",
218
+ route_name: `${cliBinName} ${commandName}`,
219
+ controller_class: controllerClass,
186
220
  action_name: actionName,
187
221
  controller_file: relPath,
188
- controller_line: 1,
222
+ controller_line: handlerLine,
189
223
  architecture_style: "cli",
190
224
  entry_kind: "cli_command",
191
225
  execution_chain_json: JSON.stringify(cliChain),
@@ -196,4 +230,99 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
196
230
 
197
231
  return slices;
198
232
  }
233
+
234
+ private resolveProjectNaming(projectRoot: string): { toolPrefix: string; cliBinName: string } {
235
+ const pkgPath = path.join(projectRoot, "package.json");
236
+ if (fs.existsSync(pkgPath)) {
237
+ try {
238
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
239
+ const rawName = typeof pkg.name === "string" ? pkg.name.replace(/^@[^/]+\//, "") : "mcp";
240
+ const toolPrefix = rawName.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase() || "mcp";
241
+ let cliBinName = rawName;
242
+ if (typeof pkg.bin === "string") {
243
+ cliBinName = rawName;
244
+ } else if (pkg.bin && typeof pkg.bin === "object") {
245
+ const firstBin = Object.keys(pkg.bin)[0];
246
+ if (firstBin) cliBinName = firstBin;
247
+ }
248
+ return { toolPrefix, cliBinName };
249
+ } catch {}
250
+ }
251
+ return { toolPrefix: "mcp", cliBinName: "cli" };
252
+ }
253
+
254
+ private scanRegisteredMcpToolNames(projectRoot: string): Map<string, string> {
255
+ const map = new Map<string, string>();
256
+ const serverPath = path.join(projectRoot, "src/mcp/server.ts");
257
+ if (!fs.existsSync(serverPath)) return map;
258
+
259
+ try {
260
+ const content = fs.readFileSync(serverPath, "utf-8");
261
+ const nameRegex = /name:\s*['"]([a-zA-Z0-9_-]+)['"]/g;
262
+ let match: RegExpExecArray | null;
263
+ while ((match = nameRegex.exec(content)) !== null) {
264
+ const fullToolName = match[1];
265
+ const normalized = fullToolName.replace(/-/g, "_");
266
+ map.set(normalized, fullToolName);
267
+ const withoutPrefix = normalized.replace(/^[a-z0-9]+_/, "");
268
+ if (withoutPrefix) {
269
+ map.set(withoutPrefix, fullToolName);
270
+ }
271
+ }
272
+ } catch {}
273
+ return map;
274
+ }
275
+
276
+ private resolveServiceDependency(
277
+ projectRoot: string,
278
+ sourceFile: string,
279
+ content: string
280
+ ): { symbol: string; file?: string } | null {
281
+ const importRegex = /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
282
+ let match: RegExpExecArray | null;
283
+
284
+ while ((match = importRegex.exec(content)) !== null) {
285
+ const clause = match[1];
286
+ const spec = match[2];
287
+ const identifiers = clause
288
+ .split(",")
289
+ .map((s) => s.trim().split(/\s+as\s+/)[0].trim())
290
+ .filter(Boolean);
291
+
292
+ for (const id of identifiers) {
293
+ if (
294
+ /(?:Repository|Service|Manager|Loader|Pipeline|Evaluator|Tracer|Detector|Engine|Store)$/.test(
295
+ id
296
+ )
297
+ ) {
298
+ const resolvedFile = this.resolveImportFile(projectRoot, sourceFile, spec);
299
+ return {
300
+ symbol: id,
301
+ ...(resolvedFile ? { file: resolvedFile } : {}),
302
+ };
303
+ }
304
+ }
305
+ }
306
+
307
+ return null;
308
+ }
309
+
310
+ private resolveImportFile(projectRoot: string, sourceFile: string, importSpec: string): string | undefined {
311
+ if (!importSpec.startsWith(".")) return undefined;
312
+ const baseResolved = path.resolve(path.dirname(sourceFile), importSpec);
313
+ const candidates = [
314
+ baseResolved,
315
+ `${baseResolved}.ts`,
316
+ `${baseResolved}.js`,
317
+ baseResolved.replace(/\.js$/, ".ts"),
318
+ path.join(baseResolved, "index.ts"),
319
+ ];
320
+ for (const c of candidates) {
321
+ if (fs.existsSync(c) && fs.statSync(c).isFile()) {
322
+ return path.relative(projectRoot, c).replace(/\\/g, "/");
323
+ }
324
+ }
325
+ return undefined;
326
+ }
199
327
  }
328
+
@@ -31,18 +31,27 @@ export class NestJsSemanticExtractor implements SemanticSliceExtractor {
31
31
  const controllerFiles = this.findFilesRecursively(srcDir, /\.controller\.(ts|js)$/);
32
32
 
33
33
  for (const file of controllerFiles) {
34
- const relPath = path.relative(projectRoot, file);
34
+ const relPath = path.relative(projectRoot, file).replace(/\\/g, "/");
35
35
  const content = fs.readFileSync(file, "utf-8");
36
36
 
37
37
  // Extract @Controller('prefix')
38
- const controllerMatch = content.match(/@Controller\s*\(\s*['"]?([^'")]*)['"]?\s*\)\s*export\s+class\s+([A-Za-z0-9_]+)/);
38
+ const controllerMatch = /@Controller\s*\(\s*['"]?([^'")]*)['"]?\s*\)[\s\S]*?export\s+class\s+([A-Za-z0-9_]+)/.exec(
39
+ content
40
+ );
39
41
  if (!controllerMatch) continue;
40
42
 
41
43
  const routePrefix = controllerMatch[1] ? `/${controllerMatch[1].replace(/^\/+|\/+$/g, "")}` : "";
42
44
  const controllerClass = controllerMatch[2];
45
+ const controllerClassLine =
46
+ controllerMatch.index !== undefined
47
+ ? content
48
+ .slice(0, controllerMatch.index + controllerMatch[0].lastIndexOf(controllerClass))
49
+ .split("\n").length
50
+ : 1;
43
51
 
44
52
  // Extract methods: @Get('path'), @Post('path'), etc.
45
- const methodRegex = /@(Get|Post|Put|Patch|Delete)\s*\(\s*['"]?([^'")]*)['"]?\s*\)\s*(?:@[A-Za-z0-9_]+\s*\([^)]*\)\s*)*\s*(?:async\s+)?([a-zA-Z0-9_]+)\s*\(([^)]*)\)/g;
53
+ const methodRegex =
54
+ /@(Get|Post|Put|Patch|Delete)\s*\(\s*['"]?([^'")]*)['"]?\s*\)\s*(?:@[A-Za-z0-9_]+\s*\([^)]*\)\s*)*\s*(?:(?:public|protected|private)\s+)?(?:async\s+)?([a-zA-Z0-9_]+)\s*\(([^)]*)\)/g;
46
55
  let match: RegExpExecArray | null;
47
56
 
48
57
  while ((match = methodRegex.exec(content)) !== null) {
@@ -52,6 +61,10 @@ export class NestJsSemanticExtractor implements SemanticSliceExtractor {
52
61
  const actionName = match[3];
53
62
  const params = match[4];
54
63
 
64
+ const decoratorLine = content.slice(0, match.index).split("\n").length;
65
+ const actionOffset = match.index + match[0].lastIndexOf(actionName);
66
+ const actionLine = content.slice(0, actionOffset).split("\n").length || controllerClassLine;
67
+
55
68
  // Extract DTO from parameters: e.g. @Body() createDto: CreateUserDto
56
69
  const dtoMatch = params.match(/@Body\s*\([^)]*\)\s*[a-zA-Z0-9_]+\s*:\s*([A-Za-z0-9_]+Dto)/);
57
70
  const formRequestClass = dtoMatch ? dtoMatch[1] : null;
@@ -61,6 +74,7 @@ export class NestJsSemanticExtractor implements SemanticSliceExtractor {
61
74
  stage: "ingress",
62
75
  symbol: `${httpMethod} ${fullUri}`,
63
76
  file: relPath,
77
+ line: decoratorLine,
64
78
  description: `NestJS Route: ${httpMethod} ${fullUri}`,
65
79
  },
66
80
  ];
@@ -77,7 +91,7 @@ export class NestJsSemanticExtractor implements SemanticSliceExtractor {
77
91
  stage: "controller",
78
92
  symbol: `${controllerClass}::${actionName}`,
79
93
  file: relPath,
80
- line: 1,
94
+ line: actionLine,
81
95
  description: `NestJS Controller Action`,
82
96
  });
83
97
 
@@ -90,7 +104,7 @@ export class NestJsSemanticExtractor implements SemanticSliceExtractor {
90
104
  controller_class: controllerClass,
91
105
  action_name: actionName,
92
106
  controller_file: relPath,
93
- controller_line: 1,
107
+ controller_line: actionLine,
94
108
  architecture_style: "mvc",
95
109
  entry_kind: "http_route",
96
110
  execution_chain_json: JSON.stringify(chain),
@@ -98,6 +112,7 @@ export class NestJsSemanticExtractor implements SemanticSliceExtractor {
98
112
  }
99
113
  }
100
114
 
115
+
101
116
  return slices;
102
117
  }
103
118