septum 0.1.2 → 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 +7 -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/cli/templates/skill.ts +7 -4
- package/src/core/config/loader.ts +21 -8
- package/src/core/database/repositories/dependency.repository.ts +70 -0
- package/src/core/database/repositories/environment.repository.ts +207 -0
- package/src/core/database/repositories/file.repository.ts +7 -0
- package/src/core/database/repositories/symbol.repository.ts +28 -9
- package/src/core/database/repository.ts +19 -1
- package/src/core/database/schema.ts +153 -120
- package/src/core/discovery/topology-detector.ts +71 -3
- package/src/core/ingestion/pipeline.ts +98 -1
- 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 +2 -0
- package/src/core/parser/extractors/blade.ts +158 -0
- package/src/core/parser/extractors/go.ts +5 -1
- package/src/core/parser/extractors/laravel-semantic.ts +506 -31
- package/src/core/parser/extractors/php.ts +5 -1
- package/src/core/parser/extractors/python.ts +3 -1
- package/src/core/parser/extractors/registry.ts +21 -0
- 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/parser/tree-sitter.ts +2 -0
- package/src/core/resolver/path-resolver.ts +147 -0
- package/src/core/resolver/vertical-slice-tracer.ts +56 -8
- 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 +264 -46
- package/src/mcp/server.ts +130 -26
- package/src/mcp/tools/check-boundary.ts +58 -8
- package/src/mcp/tools/get-domain-catalog.ts +31 -1
- package/src/mcp/tools/get-environment-topology.ts +141 -0
- package/src/mcp/tools/get-symbol-hotspots.ts +13 -3
- package/src/mcp/tools/locate-symbol.ts +119 -3
- package/src/mcp/tools/register-domain.ts +1 -1
- package/src/mcp/tools/trace-vertical-slice.ts +11 -65
- package/src/types/index.ts +20 -1
- package/src/version.ts +3 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export interface MethodBehavioralSummary {
|
|
2
|
+
guards: string[];
|
|
3
|
+
hasTransaction: boolean;
|
|
4
|
+
emittedEvents: string[];
|
|
5
|
+
dispatchedJobs: string[];
|
|
6
|
+
mutations: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class BehavioralAnalyzer {
|
|
10
|
+
/**
|
|
11
|
+
* Analyzes the body of a method or function to extract semantic side-effects,
|
|
12
|
+
* authorization guards, database transactions, emitted events, and background job dispatches.
|
|
13
|
+
*/
|
|
14
|
+
public static analyzeMethod(code: string): MethodBehavioralSummary {
|
|
15
|
+
const guards = new Set<string>();
|
|
16
|
+
let hasTransaction = false;
|
|
17
|
+
const emittedEvents = new Set<string>();
|
|
18
|
+
const dispatchedJobs = new Set<string>();
|
|
19
|
+
const mutations = new Set<string>();
|
|
20
|
+
|
|
21
|
+
const lines = code.split("\n");
|
|
22
|
+
|
|
23
|
+
for (const rawLine of lines) {
|
|
24
|
+
const line = rawLine.trim();
|
|
25
|
+
if (!line || line.startsWith("//") || line.startsWith("#")) continue;
|
|
26
|
+
|
|
27
|
+
// 1. Transaction Boundaries
|
|
28
|
+
if (
|
|
29
|
+
line.includes("DB::transaction") ||
|
|
30
|
+
line.includes("DB::beginTransaction") ||
|
|
31
|
+
line.includes("this.db.transaction") ||
|
|
32
|
+
line.includes(".transaction(")
|
|
33
|
+
) {
|
|
34
|
+
hasTransaction = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Authorization Guards
|
|
38
|
+
// Laravel: $this->authorize('update', $order); or Gate::authorize(...)
|
|
39
|
+
const authMatch = line.match(/(?:\$this->authorize|Gate::authorize)\s*\(\s*['"]([^'"]+)['"]/);
|
|
40
|
+
if (authMatch) {
|
|
41
|
+
guards.add(authMatch[1]);
|
|
42
|
+
}
|
|
43
|
+
// Laravel: abort_if(!..., 403, '...');
|
|
44
|
+
if (line.includes("abort_if(") || line.includes("abort_unless(")) {
|
|
45
|
+
guards.add("abort_guard");
|
|
46
|
+
}
|
|
47
|
+
// TS / NestJS: @UseGuards(AuthGuard, RolesGuard)
|
|
48
|
+
const guardDecoratorMatch = line.match(/@UseGuards\s*\(([^)]+)\)/);
|
|
49
|
+
if (guardDecoratorMatch) {
|
|
50
|
+
const guardNames = guardDecoratorMatch[1].split(",").map((g) => g.trim());
|
|
51
|
+
for (const g of guardNames) guards.add(g);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. Emitted Events
|
|
55
|
+
// Laravel: event(new OrderPlaced($order));
|
|
56
|
+
const eventNewMatch = line.match(/event\s*\(\s*new\s+([A-Za-z0-9_]+)/);
|
|
57
|
+
if (eventNewMatch) {
|
|
58
|
+
emittedEvents.add(eventNewMatch[1]);
|
|
59
|
+
}
|
|
60
|
+
// Laravel: Event::dispatch(new OrderPlaced($order)); or Event::dispatch('name');
|
|
61
|
+
const eventDispatchMatch = line.match(/Event::dispatch\s*\(\s*(?:new\s+)?([A-Za-z0-9_]+)/);
|
|
62
|
+
if (eventDispatchMatch) {
|
|
63
|
+
emittedEvents.add(eventDispatchMatch[1]);
|
|
64
|
+
}
|
|
65
|
+
// Laravel: broadcast(new OrderUpdated($order));
|
|
66
|
+
const broadcastMatch = line.match(/broadcast\s*\(\s*new\s+([A-Za-z0-9_]+)/);
|
|
67
|
+
if (broadcastMatch) {
|
|
68
|
+
emittedEvents.add(broadcastMatch[1]);
|
|
69
|
+
}
|
|
70
|
+
// EventEmitter: this.eventEmitter.emit('order.created', ...)
|
|
71
|
+
const nodeEmitMatch = line.match(/(?:eventEmitter|\$emit)\.emit\s*\(\s*['"]([^'"]+)['"]/);
|
|
72
|
+
if (nodeEmitMatch) {
|
|
73
|
+
emittedEvents.add(nodeEmitMatch[1]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Dispatched Background Jobs
|
|
77
|
+
// Laravel: ProcessPaymentJob::dispatch($order); or ProcessPayment::dispatchSync(...)
|
|
78
|
+
const jobDispatchStaticMatch = line.match(/([A-Z][a-zA-Z0-9_]*(?:Job|Queue|Task|Process)[a-zA-Z0-9_]*)::dispatch/);
|
|
79
|
+
if (jobDispatchStaticMatch) {
|
|
80
|
+
dispatchedJobs.add(jobDispatchStaticMatch[1]);
|
|
81
|
+
}
|
|
82
|
+
// Laravel: dispatch(new ProcessPayment($order));
|
|
83
|
+
const dispatchNewMatch = line.match(/dispatch\s*\(\s*new\s+([A-Za-z0-9_]+)/);
|
|
84
|
+
if (dispatchNewMatch) {
|
|
85
|
+
dispatchedJobs.add(dispatchNewMatch[1]);
|
|
86
|
+
}
|
|
87
|
+
// BullMQ / Queue: queue.add('process-payment', ...)
|
|
88
|
+
const queueAddMatch = line.match(/(?:queue|queueService)\.add\s*\(\s*['"]([^'"]+)['"]/);
|
|
89
|
+
if (queueAddMatch) {
|
|
90
|
+
dispatchedJobs.add(queueAddMatch[1]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 5. Model Mutations
|
|
94
|
+
// Laravel / ORM: $order->update([...]);
|
|
95
|
+
const updateMatch = line.match(/\$([a-zA-Z0-9_]+)->update\s*\(/);
|
|
96
|
+
if (updateMatch) {
|
|
97
|
+
mutations.add(`${updateMatch[1]}.update()`);
|
|
98
|
+
}
|
|
99
|
+
// Laravel / ORM: $order->delete();
|
|
100
|
+
const deleteMatch = line.match(/\$([a-zA-Z0-9_]+)->delete\s*\(/);
|
|
101
|
+
if (deleteMatch) {
|
|
102
|
+
mutations.add(`${deleteMatch[1]}.delete()`);
|
|
103
|
+
}
|
|
104
|
+
// Direct property assignment: $order->status = 'completed';
|
|
105
|
+
const assignMatch = line.match(/\$([a-zA-Z0-9_]+)->([a-zA-Z0-9_]+)\s*=\s*([^;]+);/);
|
|
106
|
+
if (assignMatch && !["this", "request"].includes(assignMatch[1])) {
|
|
107
|
+
mutations.add(`${assignMatch[1]}.${assignMatch[2]}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
guards: Array.from(guards),
|
|
113
|
+
hasTransaction,
|
|
114
|
+
emittedEvents: Array.from(emittedEvents),
|
|
115
|
+
dispatchedJobs: Array.from(dispatchedJobs),
|
|
116
|
+
mutations: Array.from(mutations),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -126,3 +126,163 @@ export function findPythonBlockEnd(lines: string[], startLineIndex: number): num
|
|
|
126
126
|
|
|
127
127
|
return lastContentLine;
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Calculates the maximum control flow nesting depth within a code block (PHP, TS, JS, Go).
|
|
132
|
+
* Base depth is 1 (the function body itself).
|
|
133
|
+
* Nested control structures (if, else if, for, foreach, while, do, switch, try, catch)
|
|
134
|
+
* that open a block increase the active depth.
|
|
135
|
+
*/
|
|
136
|
+
export function calculateControlFlowNesting(
|
|
137
|
+
lines: string[],
|
|
138
|
+
startLineIndex: number,
|
|
139
|
+
endLineIndex: number
|
|
140
|
+
): number {
|
|
141
|
+
if (startLineIndex >= endLineIndex) return 1;
|
|
142
|
+
|
|
143
|
+
let maxDepth = 1;
|
|
144
|
+
let currentDepth = 1;
|
|
145
|
+
let inMultiLineComment = false;
|
|
146
|
+
let insideBody = false;
|
|
147
|
+
|
|
148
|
+
const controlStack: number[] = [];
|
|
149
|
+
let braceDepth = 0;
|
|
150
|
+
let pendingControl = false;
|
|
151
|
+
|
|
152
|
+
const controlRegex = /\b(if|else\s+if|else|for|foreach|while|do|switch|try|catch)\b/;
|
|
153
|
+
|
|
154
|
+
const startIdx = Math.max(0, startLineIndex);
|
|
155
|
+
const endIdx = Math.min(lines.length - 1, endLineIndex);
|
|
156
|
+
|
|
157
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
158
|
+
const rawLine = lines[i];
|
|
159
|
+
|
|
160
|
+
let cleanLine = "";
|
|
161
|
+
let inSingleQuote = false;
|
|
162
|
+
let inDoubleQuote = false;
|
|
163
|
+
let inTemplateString = false;
|
|
164
|
+
|
|
165
|
+
for (let j = 0; j < rawLine.length; j++) {
|
|
166
|
+
const char = rawLine[j];
|
|
167
|
+
const prevChar = j > 0 ? rawLine[j - 1] : "";
|
|
168
|
+
const nextChar = j + 1 < rawLine.length ? rawLine[j + 1] : "";
|
|
169
|
+
|
|
170
|
+
if (inMultiLineComment) {
|
|
171
|
+
if (char === "*" && nextChar === "/") {
|
|
172
|
+
inMultiLineComment = false;
|
|
173
|
+
j++;
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
|
|
179
|
+
inMultiLineComment = true;
|
|
180
|
+
j++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
|
|
185
|
+
break; // Single line comment: ignore rest of line
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
|
|
189
|
+
inSingleQuote = !inSingleQuote;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
|
|
193
|
+
inDoubleQuote = !inDoubleQuote;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
|
|
197
|
+
inTemplateString = !inTemplateString;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (inSingleQuote || inDoubleQuote || inTemplateString) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
cleanLine += char;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const trimmedClean = cleanLine.trim();
|
|
209
|
+
if (!trimmedClean) continue;
|
|
210
|
+
|
|
211
|
+
if (insideBody && controlRegex.test(trimmedClean)) {
|
|
212
|
+
pendingControl = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (let j = 0; j < cleanLine.length; j++) {
|
|
216
|
+
const char = cleanLine[j];
|
|
217
|
+
if (char === "{") {
|
|
218
|
+
braceDepth++;
|
|
219
|
+
if (!insideBody) {
|
|
220
|
+
insideBody = true;
|
|
221
|
+
} else if (pendingControl) {
|
|
222
|
+
currentDepth++;
|
|
223
|
+
controlStack.push(braceDepth);
|
|
224
|
+
if (currentDepth > maxDepth) {
|
|
225
|
+
maxDepth = currentDepth;
|
|
226
|
+
}
|
|
227
|
+
pendingControl = false;
|
|
228
|
+
}
|
|
229
|
+
} else if (char === "}") {
|
|
230
|
+
if (controlStack.length > 0 && controlStack[controlStack.length - 1] === braceDepth) {
|
|
231
|
+
controlStack.pop();
|
|
232
|
+
currentDepth = Math.max(1, currentDepth - 1);
|
|
233
|
+
}
|
|
234
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (trimmedClean.endsWith(";")) {
|
|
239
|
+
pendingControl = false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return maxDepth;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Calculates the maximum control flow nesting depth in indentation-based languages (Python).
|
|
248
|
+
*/
|
|
249
|
+
export function calculatePythonNesting(
|
|
250
|
+
lines: string[],
|
|
251
|
+
startLineIndex: number,
|
|
252
|
+
endLineIndex: number
|
|
253
|
+
): number {
|
|
254
|
+
if (startLineIndex >= endLineIndex) return 1;
|
|
255
|
+
|
|
256
|
+
const startLine = lines[startLineIndex];
|
|
257
|
+
const baseIndentMatch = startLine.match(/^(\s*)/);
|
|
258
|
+
const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
|
|
259
|
+
|
|
260
|
+
const controlRegex = /^\s*(if|elif|else|for|while|try|except|finally|match|case)\b/;
|
|
261
|
+
const indentStack: number[] = [baseIndent];
|
|
262
|
+
let maxDepth = 1;
|
|
263
|
+
|
|
264
|
+
const startIdx = Math.max(0, startLineIndex + 1);
|
|
265
|
+
const endIdx = Math.min(lines.length - 1, endLineIndex);
|
|
266
|
+
|
|
267
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
268
|
+
const line = lines[i];
|
|
269
|
+
const trimmed = line.trim();
|
|
270
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
271
|
+
|
|
272
|
+
const currentIndentMatch = line.match(/^(\s*)/);
|
|
273
|
+
const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
|
|
274
|
+
|
|
275
|
+
while (indentStack.length > 1 && currentIndent <= indentStack[indentStack.length - 1]) {
|
|
276
|
+
indentStack.pop();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (controlRegex.test(line)) {
|
|
280
|
+
indentStack.push(currentIndent);
|
|
281
|
+
if (indentStack.length > maxDepth) {
|
|
282
|
+
maxDepth = indentStack.length;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return maxDepth;
|
|
288
|
+
}
|
|
@@ -28,6 +28,8 @@ export function detectArchetype(filePath: string, archetypesMap: Record<string,
|
|
|
28
28
|
if (lower.includes("middleware")) return "middleware";
|
|
29
29
|
if (lower.includes("request")) return "request";
|
|
30
30
|
if (lower.includes("resource")) return "resource";
|
|
31
|
+
if (lower.includes(".blade.php") || lower.includes("views/") || lower.includes("templates/")) return "view";
|
|
32
|
+
if (lower.includes("route") || lower.includes("router")) return "route";
|
|
31
33
|
if (lower.includes("util") || lower.includes("helper")) return "util";
|
|
32
34
|
|
|
33
35
|
return "unknown";
|
|
@@ -0,0 +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,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
|
}
|