septum 0.1.3 → 0.1.4
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.
- package/README.md +5 -2
- package/package.json +1 -1
- package/src/cli/commands/slice.ts +4 -46
- package/src/cli/commands/topology.ts +119 -0
- package/src/cli/index.ts +11 -0
- package/src/core/database/repositories/environment.repository.ts +207 -0
- package/src/core/database/repositories/symbol.repository.ts +28 -9
- package/src/core/database/repository.ts +6 -1
- package/src/core/database/schema.ts +153 -120
- package/src/core/parser/behavioral-analyzer.ts +119 -0
- package/src/core/parser/boundary-tracker.ts +160 -0
- package/src/core/parser/extractors/base.ts +1 -0
- package/src/core/parser/extractors/blade.ts +158 -158
- package/src/core/parser/extractors/go.ts +5 -1
- package/src/core/parser/extractors/laravel-semantic.ts +320 -4
- package/src/core/parser/extractors/php.ts +5 -1
- package/src/core/parser/extractors/python.ts +3 -1
- package/src/core/parser/extractors/typescript.ts +5 -1
- package/src/core/parser/schema/providers/laravel-migration-provider.ts +215 -0
- package/src/core/parser/schema/providers/prisma-schema-provider.ts +89 -0
- package/src/core/parser/schema/schema-provider-registry.ts +43 -0
- package/src/core/parser/schema/schema-provider.interface.ts +33 -0
- package/src/core/resolver/path-resolver.ts +147 -147
- package/src/core/resolver/vertical-slice-tracer.ts +34 -1
- package/src/core/topology/environment-detector.ts +860 -0
- package/src/core/topology/types.ts +83 -0
- package/src/index.ts +3 -0
- package/src/mcp/schemas.ts +48 -0
- package/src/mcp/server.ts +62 -2
- package/src/mcp/tools/get-environment-topology.ts +141 -0
- package/src/mcp/tools/get-symbol-hotspots.ts +13 -3
- package/src/mcp/tools/register-domain.ts +1 -1
- package/src/mcp/tools/trace-vertical-slice.ts +9 -64
- package/src/types/index.ts +16 -0
- package/src/version.ts +3 -3
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtractedDependency,
|
|
3
|
-
ExtractedSymbol,
|
|
4
|
-
ParsedFileAST,
|
|
5
|
-
} from "../../../types/index.ts";
|
|
6
|
-
import type { CodeExtractor } from "./base.ts";
|
|
7
|
-
|
|
8
|
-
export class BladeExtractor implements CodeExtractor {
|
|
9
|
-
public canHandle(filePath: string): boolean {
|
|
10
|
-
return /\.blade\.php$/i.test(filePath);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
|
|
14
|
-
const symbols: ExtractedSymbol[] = [];
|
|
15
|
-
const dependencies: ExtractedDependency[] = [];
|
|
16
|
-
|
|
17
|
-
const lines = content.split(/\r?\n/);
|
|
18
|
-
const lineCount = lines.length;
|
|
19
|
-
|
|
20
|
-
// 1. Register the view itself as a top-level symbol
|
|
21
|
-
const viewName = this.normalizeViewName(filePath);
|
|
22
|
-
if (viewName) {
|
|
23
|
-
symbols.push({
|
|
24
|
-
name: viewName,
|
|
25
|
-
kind: "view",
|
|
26
|
-
signature: `@view(${viewName})`,
|
|
27
|
-
visibility: "public",
|
|
28
|
-
line_start: 1,
|
|
29
|
-
line_end: Math.max(1, lineCount),
|
|
30
|
-
line_count: Math.max(1, lineCount),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Also register with view: prefix for explicit queries
|
|
34
|
-
symbols.push({
|
|
35
|
-
name: `view:${viewName}`,
|
|
36
|
-
kind: "view",
|
|
37
|
-
signature: `@view(${viewName})`,
|
|
38
|
-
visibility: "public",
|
|
39
|
-
line_start: 1,
|
|
40
|
-
line_end: Math.max(1, lineCount),
|
|
41
|
-
line_count: Math.max(1, lineCount),
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 2. Line-by-line scanning for Blade directives, components, and DOM IDs
|
|
46
|
-
for (let i = 0; i < lines.length; i++) {
|
|
47
|
-
const line = lines[i];
|
|
48
|
-
const trimmed = line.trim();
|
|
49
|
-
const lineNum = i + 1;
|
|
50
|
-
|
|
51
|
-
if (!trimmed) continue;
|
|
52
|
-
|
|
53
|
-
// Extends: @extends('layouts.app')
|
|
54
|
-
const extendsMatch = /@extends\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(trimmed);
|
|
55
|
-
if (extendsMatch) {
|
|
56
|
-
dependencies.push({
|
|
57
|
-
target: extendsMatch[1],
|
|
58
|
-
statement: trimmed,
|
|
59
|
-
line_number: lineNum,
|
|
60
|
-
is_external: false,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Includes: @include('partials.store_card'), @includeIf, @includeWhen, @includeUnless
|
|
65
|
-
const includeRegex = /@include(?:If|When|Unless)?\s*\(\s*['"]([^'"]+)['"]/g;
|
|
66
|
-
let incMatch: RegExpExecArray | null;
|
|
67
|
-
while ((incMatch = includeRegex.exec(trimmed)) !== null) {
|
|
68
|
-
dependencies.push({
|
|
69
|
-
target: incMatch[1],
|
|
70
|
-
statement: trimmed,
|
|
71
|
-
line_number: lineNum,
|
|
72
|
-
is_external: false,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Blade Components: <x-store.search-bar or <x-alert
|
|
77
|
-
const componentRegex = /<x-([a-zA-Z0-9_.\-]+)/g;
|
|
78
|
-
let compMatch: RegExpExecArray | null;
|
|
79
|
-
while ((compMatch = componentRegex.exec(trimmed)) !== null) {
|
|
80
|
-
const compTag = compMatch[1];
|
|
81
|
-
// Convert foo-bar to components.foo-bar or components.foo.bar
|
|
82
|
-
const compTarget = `components.${compTag.replace(/-/g, ".")}`;
|
|
83
|
-
dependencies.push({
|
|
84
|
-
target: compTarget,
|
|
85
|
-
statement: trimmed,
|
|
86
|
-
line_number: lineNum,
|
|
87
|
-
is_external: false,
|
|
88
|
-
});
|
|
89
|
-
symbols.push({
|
|
90
|
-
name: `<x-${compTag}>`,
|
|
91
|
-
kind: "component",
|
|
92
|
-
signature: `<x-${compTag}>`,
|
|
93
|
-
visibility: "public",
|
|
94
|
-
line_start: lineNum,
|
|
95
|
-
line_end: lineNum,
|
|
96
|
-
line_count: 1,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Livewire: @livewire('search-dropdown')
|
|
101
|
-
const livewireMatch = /@livewire\s*\(\s*['"]([^'"]+)['"]/.exec(trimmed);
|
|
102
|
-
if (livewireMatch) {
|
|
103
|
-
dependencies.push({
|
|
104
|
-
target: `livewire:${livewireMatch[1]}`,
|
|
105
|
-
statement: trimmed,
|
|
106
|
-
line_number: lineNum,
|
|
107
|
-
is_external: false,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Section: @section('content')
|
|
112
|
-
const sectionMatch = /@section\s*\(\s*['"]([^'"]+)['"]/.exec(trimmed);
|
|
113
|
-
if (sectionMatch) {
|
|
114
|
-
symbols.push({
|
|
115
|
-
name: `section:${sectionMatch[1]}`,
|
|
116
|
-
kind: "property",
|
|
117
|
-
signature: `@section('${sectionMatch[1]}')`,
|
|
118
|
-
visibility: "public",
|
|
119
|
-
line_start: lineNum,
|
|
120
|
-
line_end: lineNum,
|
|
121
|
-
line_count: 1,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// DOM ID attribute: id="laptop-slider" or id='laptop-slider'
|
|
126
|
-
const idMatch = /\bid=["']([a-zA-Z0-9_\-]+)["']/.exec(trimmed);
|
|
127
|
-
if (idMatch) {
|
|
128
|
-
const elementId = idMatch[1];
|
|
129
|
-
symbols.push({
|
|
130
|
-
name: `#${elementId}`,
|
|
131
|
-
kind: "property",
|
|
132
|
-
signature: `id="${elementId}"`,
|
|
133
|
-
visibility: "public",
|
|
134
|
-
line_start: lineNum,
|
|
135
|
-
line_end: lineNum,
|
|
136
|
-
line_count: 1,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return { symbols, dependencies };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Derive Laravel view name from path, e.g.:
|
|
146
|
-
* resources/views/partials/store_card.blade.php -> partials.store_card
|
|
147
|
-
*/
|
|
148
|
-
private normalizeViewName(filePath: string): string | null {
|
|
149
|
-
const clean = filePath.replace(/\\/g, "/");
|
|
150
|
-
const viewsIdx = clean.indexOf("views/");
|
|
151
|
-
if (viewsIdx !== -1) {
|
|
152
|
-
const sub = clean.slice(viewsIdx + 6).replace(/\.blade\.php$/i, "");
|
|
153
|
-
return sub.replace(/\//g, ".");
|
|
154
|
-
}
|
|
155
|
-
const base = clean.split("/").pop()?.replace(/\.blade\.php$/i, "");
|
|
156
|
-
return base || null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
ExtractedDependency,
|
|
3
|
+
ExtractedSymbol,
|
|
4
|
+
ParsedFileAST,
|
|
5
|
+
} from "../../../types/index.ts";
|
|
6
|
+
import type { CodeExtractor } from "./base.ts";
|
|
7
|
+
|
|
8
|
+
export class BladeExtractor implements CodeExtractor {
|
|
9
|
+
public canHandle(filePath: string): boolean {
|
|
10
|
+
return /\.blade\.php$/i.test(filePath);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
|
|
14
|
+
const symbols: ExtractedSymbol[] = [];
|
|
15
|
+
const dependencies: ExtractedDependency[] = [];
|
|
16
|
+
|
|
17
|
+
const lines = content.split(/\r?\n/);
|
|
18
|
+
const lineCount = lines.length;
|
|
19
|
+
|
|
20
|
+
// 1. Register the view itself as a top-level symbol
|
|
21
|
+
const viewName = this.normalizeViewName(filePath);
|
|
22
|
+
if (viewName) {
|
|
23
|
+
symbols.push({
|
|
24
|
+
name: viewName,
|
|
25
|
+
kind: "view",
|
|
26
|
+
signature: `@view(${viewName})`,
|
|
27
|
+
visibility: "public",
|
|
28
|
+
line_start: 1,
|
|
29
|
+
line_end: Math.max(1, lineCount),
|
|
30
|
+
line_count: Math.max(1, lineCount),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Also register with view: prefix for explicit queries
|
|
34
|
+
symbols.push({
|
|
35
|
+
name: `view:${viewName}`,
|
|
36
|
+
kind: "view",
|
|
37
|
+
signature: `@view(${viewName})`,
|
|
38
|
+
visibility: "public",
|
|
39
|
+
line_start: 1,
|
|
40
|
+
line_end: Math.max(1, lineCount),
|
|
41
|
+
line_count: Math.max(1, lineCount),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Line-by-line scanning for Blade directives, components, and DOM IDs
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
const line = lines[i];
|
|
48
|
+
const trimmed = line.trim();
|
|
49
|
+
const lineNum = i + 1;
|
|
50
|
+
|
|
51
|
+
if (!trimmed) continue;
|
|
52
|
+
|
|
53
|
+
// Extends: @extends('layouts.app')
|
|
54
|
+
const extendsMatch = /@extends\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(trimmed);
|
|
55
|
+
if (extendsMatch) {
|
|
56
|
+
dependencies.push({
|
|
57
|
+
target: extendsMatch[1],
|
|
58
|
+
statement: trimmed,
|
|
59
|
+
line_number: lineNum,
|
|
60
|
+
is_external: false,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Includes: @include('partials.store_card'), @includeIf, @includeWhen, @includeUnless
|
|
65
|
+
const includeRegex = /@include(?:If|When|Unless)?\s*\(\s*['"]([^'"]+)['"]/g;
|
|
66
|
+
let incMatch: RegExpExecArray | null;
|
|
67
|
+
while ((incMatch = includeRegex.exec(trimmed)) !== null) {
|
|
68
|
+
dependencies.push({
|
|
69
|
+
target: incMatch[1],
|
|
70
|
+
statement: trimmed,
|
|
71
|
+
line_number: lineNum,
|
|
72
|
+
is_external: false,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Blade Components: <x-store.search-bar or <x-alert
|
|
77
|
+
const componentRegex = /<x-([a-zA-Z0-9_.\-]+)/g;
|
|
78
|
+
let compMatch: RegExpExecArray | null;
|
|
79
|
+
while ((compMatch = componentRegex.exec(trimmed)) !== null) {
|
|
80
|
+
const compTag = compMatch[1];
|
|
81
|
+
// Convert foo-bar to components.foo-bar or components.foo.bar
|
|
82
|
+
const compTarget = `components.${compTag.replace(/-/g, ".")}`;
|
|
83
|
+
dependencies.push({
|
|
84
|
+
target: compTarget,
|
|
85
|
+
statement: trimmed,
|
|
86
|
+
line_number: lineNum,
|
|
87
|
+
is_external: false,
|
|
88
|
+
});
|
|
89
|
+
symbols.push({
|
|
90
|
+
name: `<x-${compTag}>`,
|
|
91
|
+
kind: "component",
|
|
92
|
+
signature: `<x-${compTag}>`,
|
|
93
|
+
visibility: "public",
|
|
94
|
+
line_start: lineNum,
|
|
95
|
+
line_end: lineNum,
|
|
96
|
+
line_count: 1,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Livewire: @livewire('search-dropdown')
|
|
101
|
+
const livewireMatch = /@livewire\s*\(\s*['"]([^'"]+)['"]/.exec(trimmed);
|
|
102
|
+
if (livewireMatch) {
|
|
103
|
+
dependencies.push({
|
|
104
|
+
target: `livewire:${livewireMatch[1]}`,
|
|
105
|
+
statement: trimmed,
|
|
106
|
+
line_number: lineNum,
|
|
107
|
+
is_external: false,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Section: @section('content')
|
|
112
|
+
const sectionMatch = /@section\s*\(\s*['"]([^'"]+)['"]/.exec(trimmed);
|
|
113
|
+
if (sectionMatch) {
|
|
114
|
+
symbols.push({
|
|
115
|
+
name: `section:${sectionMatch[1]}`,
|
|
116
|
+
kind: "property",
|
|
117
|
+
signature: `@section('${sectionMatch[1]}')`,
|
|
118
|
+
visibility: "public",
|
|
119
|
+
line_start: lineNum,
|
|
120
|
+
line_end: lineNum,
|
|
121
|
+
line_count: 1,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// DOM ID attribute: id="laptop-slider" or id='laptop-slider'
|
|
126
|
+
const idMatch = /\bid=["']([a-zA-Z0-9_\-]+)["']/.exec(trimmed);
|
|
127
|
+
if (idMatch) {
|
|
128
|
+
const elementId = idMatch[1];
|
|
129
|
+
symbols.push({
|
|
130
|
+
name: `#${elementId}`,
|
|
131
|
+
kind: "property",
|
|
132
|
+
signature: `id="${elementId}"`,
|
|
133
|
+
visibility: "public",
|
|
134
|
+
line_start: lineNum,
|
|
135
|
+
line_end: lineNum,
|
|
136
|
+
line_count: 1,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { symbols, dependencies };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Derive Laravel view name from path, e.g.:
|
|
146
|
+
* resources/views/partials/store_card.blade.php -> partials.store_card
|
|
147
|
+
*/
|
|
148
|
+
private normalizeViewName(filePath: string): string | null {
|
|
149
|
+
const clean = filePath.replace(/\\/g, "/");
|
|
150
|
+
const viewsIdx = clean.indexOf("views/");
|
|
151
|
+
if (viewsIdx !== -1) {
|
|
152
|
+
const sub = clean.slice(viewsIdx + 6).replace(/\.blade\.php$/i, "");
|
|
153
|
+
return sub.replace(/\//g, ".");
|
|
154
|
+
}
|
|
155
|
+
const base = clean.split("/").pop()?.replace(/\.blade\.php$/i, "");
|
|
156
|
+
return base || null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtractedDependency, ExtractedSymbol, ParsedFileAST, Visibility } from "../../../types/index.ts";
|
|
2
|
-
import { findBraceBlockEnd } from "../boundary-tracker.ts";
|
|
2
|
+
import { findBraceBlockEnd, calculateControlFlowNesting } from "../boundary-tracker.ts";
|
|
3
3
|
import type { CodeExtractor } from "./base.ts";
|
|
4
4
|
|
|
5
5
|
const GO_STDLIB_PREFIXES = new Set([
|
|
@@ -131,6 +131,7 @@ export class GoExtractor implements CodeExtractor {
|
|
|
131
131
|
const methodName = methodMatch[1];
|
|
132
132
|
const visibility: Visibility = /^[A-Z]/.test(methodName) ? "public" : "private";
|
|
133
133
|
const endLine = findBraceBlockEnd(lines, i);
|
|
134
|
+
const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
|
|
134
135
|
|
|
135
136
|
symbols.push({
|
|
136
137
|
name: methodName,
|
|
@@ -140,6 +141,7 @@ export class GoExtractor implements CodeExtractor {
|
|
|
140
141
|
line_start: lineNum,
|
|
141
142
|
line_end: endLine,
|
|
142
143
|
line_count: Math.max(1, endLine - lineNum + 1),
|
|
144
|
+
nesting_depth: nestingDepth,
|
|
143
145
|
});
|
|
144
146
|
continue;
|
|
145
147
|
}
|
|
@@ -150,6 +152,7 @@ export class GoExtractor implements CodeExtractor {
|
|
|
150
152
|
const funcName = funcMatch[1];
|
|
151
153
|
const visibility: Visibility = /^[A-Z]/.test(funcName) ? "public" : "private";
|
|
152
154
|
const endLine = findBraceBlockEnd(lines, i);
|
|
155
|
+
const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
|
|
153
156
|
|
|
154
157
|
symbols.push({
|
|
155
158
|
name: funcName,
|
|
@@ -159,6 +162,7 @@ export class GoExtractor implements CodeExtractor {
|
|
|
159
162
|
line_start: lineNum,
|
|
160
163
|
line_end: endLine,
|
|
161
164
|
line_count: Math.max(1, endLine - lineNum + 1),
|
|
165
|
+
nesting_depth: nestingDepth,
|
|
162
166
|
});
|
|
163
167
|
}
|
|
164
168
|
}
|