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.
Files changed (35) hide show
  1. package/README.md +5 -2
  2. package/package.json +1 -1
  3. package/src/cli/commands/slice.ts +4 -46
  4. package/src/cli/commands/topology.ts +119 -0
  5. package/src/cli/index.ts +11 -0
  6. package/src/core/database/repositories/environment.repository.ts +207 -0
  7. package/src/core/database/repositories/symbol.repository.ts +28 -9
  8. package/src/core/database/repository.ts +6 -1
  9. package/src/core/database/schema.ts +153 -120
  10. package/src/core/parser/behavioral-analyzer.ts +119 -0
  11. package/src/core/parser/boundary-tracker.ts +160 -0
  12. package/src/core/parser/extractors/base.ts +1 -0
  13. package/src/core/parser/extractors/blade.ts +158 -158
  14. package/src/core/parser/extractors/go.ts +5 -1
  15. package/src/core/parser/extractors/laravel-semantic.ts +320 -4
  16. package/src/core/parser/extractors/php.ts +5 -1
  17. package/src/core/parser/extractors/python.ts +3 -1
  18. package/src/core/parser/extractors/typescript.ts +5 -1
  19. package/src/core/parser/schema/providers/laravel-migration-provider.ts +215 -0
  20. package/src/core/parser/schema/providers/prisma-schema-provider.ts +89 -0
  21. package/src/core/parser/schema/schema-provider-registry.ts +43 -0
  22. package/src/core/parser/schema/schema-provider.interface.ts +33 -0
  23. package/src/core/resolver/path-resolver.ts +147 -147
  24. package/src/core/resolver/vertical-slice-tracer.ts +34 -1
  25. package/src/core/topology/environment-detector.ts +860 -0
  26. package/src/core/topology/types.ts +83 -0
  27. package/src/index.ts +3 -0
  28. package/src/mcp/schemas.ts +48 -0
  29. package/src/mcp/server.ts +62 -2
  30. package/src/mcp/tools/get-environment-topology.ts +141 -0
  31. package/src/mcp/tools/get-symbol-hotspots.ts +13 -3
  32. package/src/mcp/tools/register-domain.ts +1 -1
  33. package/src/mcp/tools/trace-vertical-slice.ts +9 -64
  34. package/src/types/index.ts +16 -0
  35. 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
  }