septum 0.1.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/bin/septum.ts +4 -0
  4. package/package.json +62 -0
  5. package/src/cli/commands/check.ts +161 -0
  6. package/src/cli/commands/hook.ts +188 -0
  7. package/src/cli/commands/ingest.ts +29 -0
  8. package/src/cli/commands/init.ts +211 -0
  9. package/src/cli/commands/locate.ts +61 -0
  10. package/src/cli/commands/query.ts +64 -0
  11. package/src/cli/commands/serve.ts +5 -0
  12. package/src/cli/commands/slice.ts +90 -0
  13. package/src/cli/commands/sync.ts +31 -0
  14. package/src/cli/index.ts +178 -0
  15. package/src/cli/templates/hooks.ts +75 -0
  16. package/src/cli/templates/rules.ts +38 -0
  17. package/src/cli/templates/skill.ts +66 -0
  18. package/src/core/boundary/evaluator.ts +210 -0
  19. package/src/core/config/loader.ts +84 -0
  20. package/src/core/config/schema.ts +39 -0
  21. package/src/core/database/client.ts +132 -0
  22. package/src/core/database/repositories/dependency.repository.ts +279 -0
  23. package/src/core/database/repositories/domain.repository.ts +198 -0
  24. package/src/core/database/repositories/file.repository.ts +99 -0
  25. package/src/core/database/repositories/meta.repository.ts +33 -0
  26. package/src/core/database/repositories/symbol.repository.ts +424 -0
  27. package/src/core/database/repository.ts +242 -0
  28. package/src/core/database/schema.sql +92 -0
  29. package/src/core/discovery/topology-detector.ts +418 -0
  30. package/src/core/ingestion/hasher.ts +5 -0
  31. package/src/core/ingestion/pipeline.ts +438 -0
  32. package/src/core/parser/boundary-tracker.ts +128 -0
  33. package/src/core/parser/extractors/base.ts +60 -0
  34. package/src/core/parser/extractors/frontend.ts +194 -0
  35. package/src/core/parser/extractors/go.ts +176 -0
  36. package/src/core/parser/extractors/laravel-semantic.ts +426 -0
  37. package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -0
  38. package/src/core/parser/extractors/nestjs-semantic.ts +119 -0
  39. package/src/core/parser/extractors/php.ts +149 -0
  40. package/src/core/parser/extractors/python.ts +226 -0
  41. package/src/core/parser/extractors/registry.ts +49 -0
  42. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -0
  43. package/src/core/parser/extractors/typescript.ts +177 -0
  44. package/src/core/parser/tree-sitter.ts +39 -0
  45. package/src/core/resolver/call-graph-tracer.ts +668 -0
  46. package/src/core/resolver/module-resolver.ts +323 -0
  47. package/src/core/resolver/symbol-locator.ts +405 -0
  48. package/src/core/resolver/vertical-slice-tracer.ts +236 -0
  49. package/src/core/session/session-manager.ts +56 -0
  50. package/src/core/telemetry/telemetry.ts +86 -0
  51. package/src/index.ts +18 -0
  52. package/src/mcp/schemas.ts +68 -0
  53. package/src/mcp/server.ts +602 -0
  54. package/src/mcp/tools/check-boundary.ts +102 -0
  55. package/src/mcp/tools/get-domain-catalog.ts +91 -0
  56. package/src/mcp/tools/get-feature-context.ts +116 -0
  57. package/src/mcp/tools/get-symbol-hotspots.ts +49 -0
  58. package/src/mcp/tools/get-symbol-impact.ts +26 -0
  59. package/src/mcp/tools/get-symbol.ts +63 -0
  60. package/src/mcp/tools/locate-symbol.ts +74 -0
  61. package/src/mcp/tools/register-domain.ts +64 -0
  62. package/src/mcp/tools/trace-vertical-slice.ts +102 -0
  63. package/src/types/index.ts +296 -0
@@ -0,0 +1,119 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { SemanticSliceExtractor, VerticalSliceCandidate } from "./semantic-extractor.interface.ts";
4
+
5
+ export class NestJsSemanticExtractor implements SemanticSliceExtractor {
6
+ public readonly id = "nestjs";
7
+ public readonly name = "NestJS Modular Semantic Extractor";
8
+
9
+ public canHandle(projectRoot: string, framework?: string): boolean {
10
+ if (framework === "nestjs") return true;
11
+
12
+ if (fs.existsSync(path.join(projectRoot, "nest-cli.json"))) return true;
13
+
14
+ const pkgPath = path.join(projectRoot, "package.json");
15
+ if (fs.existsSync(pkgPath)) {
16
+ try {
17
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
18
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
19
+ if (deps["@nestjs/core"]) return true;
20
+ } catch {}
21
+ }
22
+
23
+ return false;
24
+ }
25
+
26
+ public extractSlices(projectRoot: string, domainId?: number): VerticalSliceCandidate[] {
27
+ const slices: VerticalSliceCandidate[] = [];
28
+ const srcDir = path.join(projectRoot, "src");
29
+ if (!fs.existsSync(srcDir)) return slices;
30
+
31
+ const controllerFiles = this.findFilesRecursively(srcDir, /\.controller\.(ts|js)$/);
32
+
33
+ for (const file of controllerFiles) {
34
+ const relPath = path.relative(projectRoot, file);
35
+ const content = fs.readFileSync(file, "utf-8");
36
+
37
+ // Extract @Controller('prefix')
38
+ const controllerMatch = content.match(/@Controller\s*\(\s*['"]?([^'")]*)['"]?\s*\)\s*export\s+class\s+([A-Za-z0-9_]+)/);
39
+ if (!controllerMatch) continue;
40
+
41
+ const routePrefix = controllerMatch[1] ? `/${controllerMatch[1].replace(/^\/+|\/+$/g, "")}` : "";
42
+ const controllerClass = controllerMatch[2];
43
+
44
+ // 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;
46
+ let match: RegExpExecArray | null;
47
+
48
+ while ((match = methodRegex.exec(content)) !== null) {
49
+ const httpMethod = match[1].toUpperCase();
50
+ const subPath = match[2] ? `/${match[2].replace(/^\/+|\/+$/g, "")}` : "";
51
+ const fullUri = `${routePrefix}${subPath}` || "/";
52
+ const actionName = match[3];
53
+ const params = match[4];
54
+
55
+ // Extract DTO from parameters: e.g. @Body() createDto: CreateUserDto
56
+ const dtoMatch = params.match(/@Body\s*\([^)]*\)\s*[a-zA-Z0-9_]+\s*:\s*([A-Za-z0-9_]+Dto)/);
57
+ const formRequestClass = dtoMatch ? dtoMatch[1] : null;
58
+
59
+ const chain: Array<{ stage: string; symbol: string; file?: string; line?: number; description?: string }> = [
60
+ {
61
+ stage: "ingress",
62
+ symbol: `${httpMethod} ${fullUri}`,
63
+ file: relPath,
64
+ description: `NestJS Route: ${httpMethod} ${fullUri}`,
65
+ },
66
+ ];
67
+
68
+ if (formRequestClass) {
69
+ chain.push({
70
+ stage: "validation",
71
+ symbol: formRequestClass,
72
+ description: "DTO Validation",
73
+ });
74
+ }
75
+
76
+ chain.push({
77
+ stage: "controller",
78
+ symbol: `${controllerClass}::${actionName}`,
79
+ file: relPath,
80
+ line: 1,
81
+ description: `NestJS Controller Action`,
82
+ });
83
+
84
+ slices.push({
85
+ domain_id: domainId ?? null,
86
+ feature_key: `nest.${controllerClass.toLowerCase().replace("controller", "")}.${actionName}`,
87
+ http_method: httpMethod,
88
+ route_uri: fullUri,
89
+ route_name: `${controllerClass}.${actionName}`,
90
+ controller_class: controllerClass,
91
+ action_name: actionName,
92
+ controller_file: relPath,
93
+ controller_line: 1,
94
+ architecture_style: "mvc",
95
+ entry_kind: "http_route",
96
+ execution_chain_json: JSON.stringify(chain),
97
+ });
98
+ }
99
+ }
100
+
101
+ return slices;
102
+ }
103
+
104
+ private findFilesRecursively(dir: string, pattern: RegExp): string[] {
105
+ const results: string[] = [];
106
+ try {
107
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
108
+ for (const entry of entries) {
109
+ const full = path.join(dir, entry.name);
110
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
111
+ results.push(...this.findFilesRecursively(full, pattern));
112
+ } else if (entry.isFile() && pattern.test(entry.name)) {
113
+ results.push(full);
114
+ }
115
+ }
116
+ } catch {}
117
+ return results;
118
+ }
119
+ }
@@ -0,0 +1,149 @@
1
+ import type { ExtractedDependency, ExtractedSymbol, ParsedFileAST, Visibility } from "../../../types/index.ts";
2
+ import { findBraceBlockEnd } from "../boundary-tracker.ts";
3
+ import type { CodeExtractor } from "./base.ts";
4
+
5
+ export class PHPExtractor implements CodeExtractor {
6
+ public canHandle(filePath: string): boolean {
7
+ return /\.php$/i.test(filePath);
8
+ }
9
+
10
+ public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
11
+ const symbols: ExtractedSymbol[] = [];
12
+ const dependencies: ExtractedDependency[] = [];
13
+
14
+ const lines = content.split("\n");
15
+
16
+ // 1. Extract PHP use / import statements
17
+ const useRegex = /^\s*use\s+([^;]+);/;
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const line = lines[i];
20
+ const match = useRegex.exec(line);
21
+ if (match) {
22
+ 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\\");
30
+
31
+ dependencies.push({
32
+ target: cleaned,
33
+ statement: line.trim(),
34
+ line_number: i + 1,
35
+ is_external: isExternal,
36
+ });
37
+ }
38
+ }
39
+
40
+ // 2. Extract PHP Class / Interface / Trait / Enum
41
+ let currentContainer: string | null = null;
42
+ let currentContainerEndLine = 0;
43
+
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = lines[i];
46
+ const trimmed = line.trim();
47
+ const lineNum = i + 1;
48
+
49
+ if (currentContainer && lineNum > currentContainerEndLine) {
50
+ currentContainer = null;
51
+ currentContainerEndLine = 0;
52
+ }
53
+
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);
56
+ if (classMatch) {
57
+ currentContainer = classMatch[2];
58
+ const endLine = findBraceBlockEnd(lines, i);
59
+ currentContainerEndLine = endLine;
60
+ symbols.push({
61
+ name: classMatch[2],
62
+ kind: classMatch[1] as any,
63
+ signature: trimmed.replace(/\{$/, "").trim(),
64
+ visibility: "public",
65
+ line_start: lineNum,
66
+ line_end: endLine,
67
+ line_count: Math.max(1, endLine - lineNum + 1),
68
+ });
69
+ continue;
70
+ }
71
+
72
+ // Methods or Properties in class/interface/trait/enum
73
+ 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);
76
+ if (constMatch) {
77
+ const vis = (constMatch[1] as Visibility) || "public";
78
+ const constName = constMatch[2];
79
+ symbols.push({
80
+ name: `${currentContainer}::${constName}`,
81
+ kind: "property",
82
+ signature: `${vis} const ${constName}`,
83
+ visibility: vis,
84
+ line_start: lineNum,
85
+ line_end: lineNum,
86
+ line_count: 1,
87
+ });
88
+ continue;
89
+ }
90
+
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);
93
+ if (propMatch && !trimmed.includes("function ")) {
94
+ const vis = (propMatch[1] as Visibility) || "public";
95
+ const propName = `$${propMatch[2]}`;
96
+ symbols.push({
97
+ name: `${currentContainer}::${propName}`,
98
+ kind: "property",
99
+ signature: `${vis} ${trimmed.replace(/;\s*$/, "")}`,
100
+ visibility: vis,
101
+ line_start: lineNum,
102
+ line_end: lineNum,
103
+ line_count: 1,
104
+ });
105
+ continue;
106
+ }
107
+
108
+ const methodMatch = /^(?:(public|protected|private)\s+)?(?:static\s+)?(?:final\s+)?function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(trimmed);
109
+ if (methodMatch) {
110
+ const vis = (methodMatch[1] as Visibility) || "public";
111
+ const name = methodMatch[2];
112
+ const params = methodMatch[3].trim();
113
+ const retType = methodMatch[4] ? `: ${methodMatch[4].trim()}` : "";
114
+ const endLine = findBraceBlockEnd(lines, i);
115
+ symbols.push({
116
+ name: `${currentContainer}::${name}`,
117
+ kind: "method",
118
+ signature: `${vis} function ${name}(${params})${retType}`,
119
+ visibility: vis,
120
+ line_start: lineNum,
121
+ line_end: endLine,
122
+ line_count: Math.max(1, endLine - lineNum + 1),
123
+ });
124
+ continue;
125
+ }
126
+ }
127
+
128
+ // Standalone Global Function in PHP
129
+ const funcMatch = /^(?:final\s+)?function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(trimmed);
130
+ if (funcMatch && !currentContainer) {
131
+ const name = funcMatch[1];
132
+ const params = funcMatch[2].trim();
133
+ const retType = funcMatch[3] ? `: ${funcMatch[3].trim()}` : "";
134
+ const endLine = findBraceBlockEnd(lines, i);
135
+ symbols.push({
136
+ name,
137
+ kind: "function",
138
+ signature: `function ${name}(${params})${retType}`,
139
+ visibility: "public",
140
+ line_start: lineNum,
141
+ line_end: endLine,
142
+ line_count: Math.max(1, endLine - lineNum + 1),
143
+ });
144
+ }
145
+ }
146
+
147
+ return { symbols, dependencies };
148
+ }
149
+ }
@@ -0,0 +1,226 @@
1
+ import type { ExtractedDependency, ExtractedSymbol, ParsedFileAST, Visibility } from "../../../types/index.ts";
2
+ import { findPythonBlockEnd } 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
+ symbols.push({
210
+ name: isMethod ? `${currentClass}::${funcName}` : funcName,
211
+ kind: isMethod ? "method" : "function",
212
+ signature: trimmed.replace(/:$/, "").trim(),
213
+ visibility,
214
+ line_start: lineNum,
215
+ line_end: endLine,
216
+ line_count: Math.max(1, endLine - lineNum + 1),
217
+ });
218
+ }
219
+ }
220
+
221
+ return {
222
+ symbols,
223
+ dependencies,
224
+ };
225
+ }
226
+ }
@@ -0,0 +1,49 @@
1
+ import type {
2
+ SemanticSliceExtractor,
3
+ VerticalSliceCandidate,
4
+ } from "./semantic-extractor.interface.ts";
5
+ import { LaravelSemanticExtractor } from "./laravel-semantic.ts";
6
+ import { McpCliSemanticExtractor } from "./mcp-cli-semantic.ts";
7
+ import { NestJsSemanticExtractor } from "./nestjs-semantic.ts";
8
+
9
+ export class SemanticSliceExtractorRegistry {
10
+ private static extractors: SemanticSliceExtractor[] = [
11
+ new LaravelSemanticExtractor(),
12
+ new McpCliSemanticExtractor(),
13
+ new NestJsSemanticExtractor(),
14
+ ];
15
+
16
+ /**
17
+ * Registers a new semantic slice extractor strategy.
18
+ * Prepends to the list to give higher priority to custom extractors.
19
+ */
20
+ public static register(extractor: SemanticSliceExtractor): void {
21
+ this.extractors.unshift(extractor);
22
+ }
23
+
24
+ /**
25
+ * Returns all extractors capable of handling the specified project topology.
26
+ */
27
+ public static getExtractors(projectRoot: string, framework?: string): SemanticSliceExtractor[] {
28
+ return this.extractors.filter((extractor) => extractor.canHandle(projectRoot, framework));
29
+ }
30
+
31
+ /**
32
+ * Executes all matched extractors and combines their vertical slices.
33
+ */
34
+ public static async extractAllSlices(
35
+ projectRoot: string,
36
+ framework?: string,
37
+ domainId?: number
38
+ ): Promise<VerticalSliceCandidate[]> {
39
+ const matched = this.getExtractors(projectRoot, framework);
40
+ const combinedSlices: VerticalSliceCandidate[] = [];
41
+
42
+ for (const extractor of matched) {
43
+ const slices = await extractor.extractSlices(projectRoot, domainId);
44
+ combinedSlices.push(...slices);
45
+ }
46
+
47
+ return combinedSlices;
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ import type { VerticalSliceRecord } from "../../../types/index.ts";
2
+
3
+ export type VerticalSliceCandidate = Omit<VerticalSliceRecord, "id" | "created_at">;
4
+
5
+ export interface SemanticSliceExtractor {
6
+ readonly id: string;
7
+ readonly name: string;
8
+
9
+ /**
10
+ * Determines if this extractor can handle the project topology.
11
+ */
12
+ canHandle(projectRoot: string, framework?: string): boolean;
13
+
14
+ /**
15
+ * Extracts vertical slices from the codebase.
16
+ */
17
+ extractSlices(
18
+ projectRoot: string,
19
+ domainId?: number
20
+ ): Promise<VerticalSliceCandidate[]> | VerticalSliceCandidate[];
21
+ }