septum 0.1.5 → 0.1.7
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 +23 -3
- package/package.json +5 -5
- package/src/cli/commands/check.ts +32 -122
- package/src/cli/commands/hook.ts +45 -12
- package/src/cli/commands/init.ts +6 -4
- package/src/cli/commands/query.ts +45 -5
- package/src/cli/commands/slice.ts +4 -2
- package/src/cli/index.ts +17 -6
- package/src/cli/templates/hooks.ts +6 -6
- package/src/cli/templates/rules.ts +35 -23
- package/src/cli/templates/skill.ts +47 -51
- package/src/core/boundary/evaluator.ts +217 -24
- package/src/core/boundary/staged-auditor.ts +47 -0
- package/src/core/config/loader.ts +29 -12
- package/src/core/database/repositories/dependency.repository.ts +252 -66
- package/src/core/database/repositories/domain.repository.ts +96 -39
- package/src/core/database/repositories/file.repository.ts +20 -19
- package/src/core/database/repositories/symbol.repository.ts +115 -5
- package/src/core/database/repository.ts +19 -2
- package/src/core/database/schema.ts +3 -2
- package/src/core/ingestion/pipeline.ts +96 -60
- package/src/core/parser/boundary-tracker.ts +145 -0
- package/src/core/parser/extractors/frontend.ts +44 -90
- package/src/core/parser/extractors/go.ts +200 -21
- package/src/core/parser/extractors/laravel-semantic.ts +18 -12
- package/src/core/parser/extractors/mcp-cli-semantic.ts +173 -44
- package/src/core/parser/extractors/nestjs-semantic.ts +20 -5
- package/src/core/parser/extractors/php.ts +293 -31
- package/src/core/parser/extractors/python.ts +242 -10
- package/src/core/parser/extractors/typescript.ts +604 -117
- package/src/core/parser/tree-sitter.ts +6 -1
- package/src/core/parser/wasm-assets.ts +17 -0
- package/src/core/parser/wasm-loader.ts +274 -0
- package/src/core/resolver/call-graph-tracer.ts +49 -17
- package/src/core/resolver/module-resolver.ts +88 -26
- package/src/core/resolver/vertical-slice-tracer.ts +34 -11
- package/src/core/session/session-manager.ts +55 -39
- package/src/core/topology/environment-detector.ts +109 -6
- package/src/mcp/schemas.ts +146 -29
- package/src/mcp/server.ts +177 -114
- package/src/mcp/tools/check-boundary.ts +159 -41
- package/src/mcp/tools/get-domain-catalog.ts +30 -15
- package/src/mcp/tools/get-environment-topology.ts +3 -6
- package/src/mcp/tools/get-feature-context.ts +35 -20
- package/src/mcp/tools/get-symbol-hotspots.ts +4 -10
- package/src/mcp/tools/locate-symbol.ts +37 -16
- package/src/mcp/tools/register-domain.ts +3 -12
- package/src/mcp/tools/register-feature.ts +25 -32
- package/src/mcp/tools/trace-vertical-slice.ts +8 -8
- package/src/types/index.ts +56 -6
- package/src/types/wasm.d.ts +4 -0
|
@@ -39,16 +39,46 @@ const GO_STDLIB_PREFIXES = new Set([
|
|
|
39
39
|
"unsafe",
|
|
40
40
|
]);
|
|
41
41
|
|
|
42
|
+
import type Parser from "web-tree-sitter";
|
|
43
|
+
import { blankCommentsAndStrings } from "../boundary-tracker.ts";
|
|
44
|
+
import { calculateAstControlFlowNesting, parseWithTreeSitter } from "../wasm-loader.ts";
|
|
45
|
+
|
|
46
|
+
const GO_CONTROL_FLOW_NODES = new Set([
|
|
47
|
+
"if_statement",
|
|
48
|
+
"for_statement",
|
|
49
|
+
"expression_switch_statement",
|
|
50
|
+
"type_switch_statement",
|
|
51
|
+
"select_statement",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const GO_FUNCTION_BOUNDARY_NODES = new Set([
|
|
55
|
+
"function_declaration",
|
|
56
|
+
"method_declaration",
|
|
57
|
+
"func_literal",
|
|
58
|
+
]);
|
|
59
|
+
|
|
42
60
|
export class GoExtractor implements CodeExtractor {
|
|
43
61
|
public canHandle(filePath: string): boolean {
|
|
44
62
|
return /\.go$/i.test(filePath);
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
public async extract(filePath: string, content: string): Promise<ParsedFileAST> {
|
|
66
|
+
const astResult = await parseWithTreeSitter("go", content, (rootNode) =>
|
|
67
|
+
this.extractFromAst(rootNode, filePath, content)
|
|
68
|
+
);
|
|
69
|
+
if (astResult) {
|
|
70
|
+
return { ...astResult, status: "parsed" };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { ...this.extractFallback(filePath, content), status: "degraded" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private extractFallback(filePath: string, content: string): ParsedFileAST {
|
|
48
77
|
const symbols: ExtractedSymbol[] = [];
|
|
49
78
|
const dependencies: ExtractedDependency[] = [];
|
|
50
79
|
|
|
51
80
|
const lines = content.split("\n");
|
|
81
|
+
const cleanLines = blankCommentsAndStrings(content).split("\n");
|
|
52
82
|
|
|
53
83
|
// 1. Extract Go Imports
|
|
54
84
|
let inImportBlock = false;
|
|
@@ -56,69 +86,74 @@ export class GoExtractor implements CodeExtractor {
|
|
|
56
86
|
for (let i = 0; i < lines.length; i++) {
|
|
57
87
|
const line = lines[i];
|
|
58
88
|
const trimmed = line.trim();
|
|
89
|
+
const cleanTrimmed = cleanLines[i].trim();
|
|
59
90
|
const lineNum = i + 1;
|
|
60
91
|
|
|
61
|
-
if (trimmed.startsWith("//")) continue;
|
|
92
|
+
if (!cleanTrimmed || trimmed.startsWith("//")) continue;
|
|
62
93
|
|
|
63
|
-
if (
|
|
94
|
+
if (cleanTrimmed.startsWith("import (")) {
|
|
64
95
|
inImportBlock = true;
|
|
65
96
|
continue;
|
|
66
97
|
}
|
|
67
98
|
|
|
68
99
|
if (inImportBlock) {
|
|
69
|
-
if (
|
|
100
|
+
if (cleanTrimmed === ")") {
|
|
70
101
|
inImportBlock = false;
|
|
71
102
|
continue;
|
|
72
103
|
}
|
|
73
104
|
|
|
74
|
-
const blockMatch = /^(?:[\w.]
|
|
105
|
+
const blockMatch = /^(?:([\w.]+)\s+)?["'`]([^"'`]+)["'`]/.exec(trimmed);
|
|
75
106
|
if (blockMatch) {
|
|
76
|
-
const
|
|
107
|
+
const alias = blockMatch[1];
|
|
108
|
+
const importPath = blockMatch[2];
|
|
77
109
|
const isExternal = this.isExternalImport(importPath, filePath);
|
|
110
|
+
const pkgSymbol = alias && alias !== "." && alias !== "_" ? alias : importPath.split("/").pop() || importPath;
|
|
78
111
|
dependencies.push({
|
|
79
112
|
target: importPath,
|
|
80
113
|
statement: trimmed,
|
|
81
114
|
line_number: lineNum,
|
|
82
115
|
is_external: isExternal,
|
|
116
|
+
imported_symbols: [pkgSymbol],
|
|
83
117
|
});
|
|
84
118
|
}
|
|
85
119
|
continue;
|
|
86
120
|
}
|
|
87
121
|
|
|
88
|
-
|
|
89
|
-
const singleMatch = /^import\s+(?:[\w.]+\s+)?["']([^"']+)["']/.exec(trimmed);
|
|
122
|
+
const singleMatch = /^import\s+(?:([\w.]+)\s+)?["'`]([^"'`]+)["'`]/.exec(trimmed);
|
|
90
123
|
if (singleMatch) {
|
|
91
|
-
const
|
|
124
|
+
const alias = singleMatch[1];
|
|
125
|
+
const importPath = singleMatch[2];
|
|
92
126
|
const isExternal = this.isExternalImport(importPath, filePath);
|
|
127
|
+
const pkgSymbol = alias && alias !== "." && alias !== "_" ? alias : importPath.split("/").pop() || importPath;
|
|
93
128
|
dependencies.push({
|
|
94
129
|
target: importPath,
|
|
95
130
|
statement: trimmed,
|
|
96
131
|
line_number: lineNum,
|
|
97
132
|
is_external: isExternal,
|
|
133
|
+
imported_symbols: [pkgSymbol],
|
|
98
134
|
});
|
|
99
135
|
}
|
|
100
136
|
}
|
|
101
137
|
|
|
102
138
|
// 2. Extract Go Types, Structs, Interfaces, and Functions
|
|
103
|
-
for (let i = 0; i <
|
|
104
|
-
const
|
|
105
|
-
const
|
|
139
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
140
|
+
const trimmed = cleanLines[i].trim();
|
|
141
|
+
const rawTrimmed = lines[i].trim();
|
|
106
142
|
const lineNum = i + 1;
|
|
107
143
|
|
|
108
|
-
if (trimmed
|
|
144
|
+
if (!trimmed) continue;
|
|
109
145
|
|
|
110
|
-
// Struct or Interface: type User struct / type Reader interface
|
|
111
146
|
const typeMatch = /^type\s+([A-Za-z0-9_]+)\s+(struct|interface)\b/.exec(trimmed);
|
|
112
147
|
if (typeMatch) {
|
|
113
148
|
const typeName = typeMatch[1];
|
|
114
149
|
const kind = typeMatch[2] === "interface" ? "interface" : "class";
|
|
115
150
|
const visibility: Visibility = /^[A-Z]/.test(typeName) ? "public" : "private";
|
|
116
|
-
const endLine = findBraceBlockEnd(
|
|
151
|
+
const endLine = findBraceBlockEnd(cleanLines, i);
|
|
117
152
|
|
|
118
153
|
symbols.push({
|
|
119
154
|
name: typeName,
|
|
120
155
|
kind,
|
|
121
|
-
signature:
|
|
156
|
+
signature: rawTrimmed.replace(/\{$/, "").trim(),
|
|
122
157
|
visibility,
|
|
123
158
|
line_start: lineNum,
|
|
124
159
|
line_end: endLine,
|
|
@@ -127,18 +162,17 @@ export class GoExtractor implements CodeExtractor {
|
|
|
127
162
|
continue;
|
|
128
163
|
}
|
|
129
164
|
|
|
130
|
-
// Method with receiver: func (r *Repo) FindUser(...) (...)
|
|
131
165
|
const methodMatch = /^func\s*\([^)]+\)\s*([A-Za-z0-9_]+)\s*\([^)]*\)/.exec(trimmed);
|
|
132
166
|
if (methodMatch) {
|
|
133
167
|
const methodName = methodMatch[1];
|
|
134
168
|
const visibility: Visibility = /^[A-Z]/.test(methodName) ? "public" : "private";
|
|
135
|
-
const endLine = findBraceBlockEnd(
|
|
169
|
+
const endLine = findBraceBlockEnd(cleanLines, i);
|
|
136
170
|
const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
|
|
137
171
|
|
|
138
172
|
symbols.push({
|
|
139
173
|
name: methodName,
|
|
140
174
|
kind: "method",
|
|
141
|
-
signature:
|
|
175
|
+
signature: rawTrimmed.replace(/\{$/, "").trim(),
|
|
142
176
|
visibility,
|
|
143
177
|
line_start: lineNum,
|
|
144
178
|
line_end: endLine,
|
|
@@ -148,18 +182,17 @@ export class GoExtractor implements CodeExtractor {
|
|
|
148
182
|
continue;
|
|
149
183
|
}
|
|
150
184
|
|
|
151
|
-
// Function: func ProcessPayment(...) (...)
|
|
152
185
|
const funcMatch = /^func\s+([A-Za-z0-9_]+)\s*\([^)]*\)/.exec(trimmed);
|
|
153
186
|
if (funcMatch) {
|
|
154
187
|
const funcName = funcMatch[1];
|
|
155
188
|
const visibility: Visibility = /^[A-Z]/.test(funcName) ? "public" : "private";
|
|
156
|
-
const endLine = findBraceBlockEnd(
|
|
189
|
+
const endLine = findBraceBlockEnd(cleanLines, i);
|
|
157
190
|
const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
|
|
158
191
|
|
|
159
192
|
symbols.push({
|
|
160
193
|
name: funcName,
|
|
161
194
|
kind: "function",
|
|
162
|
-
signature:
|
|
195
|
+
signature: rawTrimmed.replace(/\{$/, "").trim(),
|
|
163
196
|
visibility,
|
|
164
197
|
line_start: lineNum,
|
|
165
198
|
line_end: endLine,
|
|
@@ -175,6 +208,7 @@ export class GoExtractor implements CodeExtractor {
|
|
|
175
208
|
};
|
|
176
209
|
}
|
|
177
210
|
|
|
211
|
+
|
|
178
212
|
private isExternalImport(importPath: string, filePath?: string): boolean {
|
|
179
213
|
if (this.isGoStdlib(importPath)) {
|
|
180
214
|
return true;
|
|
@@ -226,4 +260,149 @@ export class GoExtractor implements CodeExtractor {
|
|
|
226
260
|
const rootSegment = importPath.split("/")[0];
|
|
227
261
|
return GO_STDLIB_PREFIXES.has(rootSegment) || !importPath.includes(".");
|
|
228
262
|
}
|
|
263
|
+
|
|
264
|
+
private extractFromAst(
|
|
265
|
+
rootNode: Parser.SyntaxNode,
|
|
266
|
+
filePath: string,
|
|
267
|
+
content: string
|
|
268
|
+
): ParsedFileAST {
|
|
269
|
+
const symbols: ExtractedSymbol[] = [];
|
|
270
|
+
const dependencies: ExtractedDependency[] = [];
|
|
271
|
+
const lines = content.split("\n");
|
|
272
|
+
|
|
273
|
+
const addImportSpec = (specNode: Parser.SyntaxNode): void => {
|
|
274
|
+
const pathNode =
|
|
275
|
+
specNode.childForFieldName("path") ??
|
|
276
|
+
specNode.namedChildren.find(
|
|
277
|
+
(c) => c.type === "interpreted_string_literal" || c.type === "raw_string_literal"
|
|
278
|
+
);
|
|
279
|
+
if (!pathNode) return;
|
|
280
|
+
|
|
281
|
+
const importPath = pathNode.text.replace(/^["'`]|["'`]$/g, "").trim();
|
|
282
|
+
if (!importPath) return;
|
|
283
|
+
|
|
284
|
+
const nameNode =
|
|
285
|
+
specNode.childForFieldName("name") ??
|
|
286
|
+
specNode.namedChildren.find((c) => c.type === "package_identifier" || c.type === "dot" || c.type === "blank_identifier");
|
|
287
|
+
const alias = nameNode?.text?.trim();
|
|
288
|
+
const pkgSymbol =
|
|
289
|
+
alias && alias !== "." && alias !== "_" ? alias : importPath.split("/").pop() || importPath;
|
|
290
|
+
|
|
291
|
+
dependencies.push({
|
|
292
|
+
target: importPath,
|
|
293
|
+
statement: (lines[specNode.startPosition.row] ?? specNode.text).trim(),
|
|
294
|
+
line_number: specNode.startPosition.row + 1,
|
|
295
|
+
is_external: this.isExternalImport(importPath, filePath),
|
|
296
|
+
imported_symbols: [pkgSymbol],
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
for (const node of rootNode.namedChildren) {
|
|
301
|
+
if (node.type === "import_declaration") {
|
|
302
|
+
for (const child of node.namedChildren) {
|
|
303
|
+
if (child.type === "import_spec") {
|
|
304
|
+
addImportSpec(child);
|
|
305
|
+
} else if (child.type === "import_spec_list") {
|
|
306
|
+
for (const spec of child.namedChildren) {
|
|
307
|
+
if (spec.type === "import_spec") {
|
|
308
|
+
addImportSpec(spec);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (node.type === "type_declaration") {
|
|
317
|
+
for (const spec of node.namedChildren) {
|
|
318
|
+
if (spec.type !== "type_spec" && spec.type !== "type_alias") continue;
|
|
319
|
+
const nameNode =
|
|
320
|
+
spec.childForFieldName("name") ??
|
|
321
|
+
spec.namedChildren.find((c) => c.type === "type_identifier");
|
|
322
|
+
if (!nameNode) continue;
|
|
323
|
+
|
|
324
|
+
const typeName = nameNode.text;
|
|
325
|
+
const typeVal = spec.childForFieldName("type") ?? spec.namedChildren[1];
|
|
326
|
+
const kind = typeVal?.type === "interface_type" ? "interface" : "class";
|
|
327
|
+
const visibility: Visibility = /^[A-Z]/.test(typeName) ? "public" : "private";
|
|
328
|
+
const lineStart = spec.startPosition.row + 1;
|
|
329
|
+
const lineEnd = Math.max(lineStart, spec.endPosition.row + 1);
|
|
330
|
+
const headerLine = (lines[spec.startPosition.row] ?? "").trim().replace(/\{$/, "").trim();
|
|
331
|
+
|
|
332
|
+
symbols.push({
|
|
333
|
+
name: typeName,
|
|
334
|
+
kind,
|
|
335
|
+
signature: headerLine || `type ${typeName}`,
|
|
336
|
+
visibility,
|
|
337
|
+
line_start: lineStart,
|
|
338
|
+
line_end: lineEnd,
|
|
339
|
+
line_count: Math.max(1, lineEnd - lineStart + 1),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (node.type === "method_declaration") {
|
|
346
|
+
const nameNode =
|
|
347
|
+
node.childForFieldName("name") ??
|
|
348
|
+
node.namedChildren.find((c) => c.type === "field_identifier" || c.type === "identifier");
|
|
349
|
+
if (!nameNode) continue;
|
|
350
|
+
|
|
351
|
+
const methodName = nameNode.text;
|
|
352
|
+
const visibility: Visibility = /^[A-Z]/.test(methodName) ? "public" : "private";
|
|
353
|
+
const lineStart = node.startPosition.row + 1;
|
|
354
|
+
const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
|
|
355
|
+
const headerLine = (lines[node.startPosition.row] ?? "").trim().replace(/\{$/, "").trim();
|
|
356
|
+
const nestingDepth = calculateAstControlFlowNesting(
|
|
357
|
+
node,
|
|
358
|
+
GO_CONTROL_FLOW_NODES,
|
|
359
|
+
GO_FUNCTION_BOUNDARY_NODES
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
symbols.push({
|
|
363
|
+
name: methodName,
|
|
364
|
+
kind: "method",
|
|
365
|
+
signature: headerLine || `func ${methodName}()`,
|
|
366
|
+
visibility,
|
|
367
|
+
line_start: lineStart,
|
|
368
|
+
line_end: lineEnd,
|
|
369
|
+
line_count: Math.max(1, lineEnd - lineStart + 1),
|
|
370
|
+
nesting_depth: nestingDepth,
|
|
371
|
+
});
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (node.type === "function_declaration") {
|
|
376
|
+
const nameNode =
|
|
377
|
+
node.childForFieldName("name") ??
|
|
378
|
+
node.namedChildren.find((c) => c.type === "identifier");
|
|
379
|
+
if (!nameNode) continue;
|
|
380
|
+
|
|
381
|
+
const funcName = nameNode.text;
|
|
382
|
+
const visibility: Visibility = /^[A-Z]/.test(funcName) ? "public" : "private";
|
|
383
|
+
const lineStart = node.startPosition.row + 1;
|
|
384
|
+
const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
|
|
385
|
+
const headerLine = (lines[node.startPosition.row] ?? "").trim().replace(/\{$/, "").trim();
|
|
386
|
+
const nestingDepth = calculateAstControlFlowNesting(
|
|
387
|
+
node,
|
|
388
|
+
GO_CONTROL_FLOW_NODES,
|
|
389
|
+
GO_FUNCTION_BOUNDARY_NODES
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
symbols.push({
|
|
393
|
+
name: funcName,
|
|
394
|
+
kind: "function",
|
|
395
|
+
signature: headerLine || `func ${funcName}()`,
|
|
396
|
+
visibility,
|
|
397
|
+
line_start: lineStart,
|
|
398
|
+
line_end: lineEnd,
|
|
399
|
+
line_count: Math.max(1, lineEnd - lineStart + 1),
|
|
400
|
+
nesting_depth: nestingDepth,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return { symbols, dependencies };
|
|
406
|
+
}
|
|
229
407
|
}
|
|
408
|
+
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { ExecutionChainNode, VerticalSliceRecord } from "../../../types/index.ts";
|
|
4
4
|
import { BehavioralAnalyzer } from "../behavioral-analyzer.ts";
|
|
5
|
+
import { blankCommentsAndStrings } from "../boundary-tracker.ts";
|
|
5
6
|
import { SchemaProviderRegistry } from "../schema/schema-provider-registry.ts";
|
|
6
7
|
import type {
|
|
7
8
|
SemanticSliceExtractor,
|
|
@@ -312,6 +313,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
312
313
|
|
|
313
314
|
// 2. Parse routes with group context tracking (prefix, controller, name prefix)
|
|
314
315
|
const lines = content.split(/\r?\n/);
|
|
316
|
+
const cleanLines = blankCommentsAndStrings(content, { hashComments: true }).split(/\r?\n/);
|
|
315
317
|
interface GroupContext {
|
|
316
318
|
prefix: string;
|
|
317
319
|
controller: string;
|
|
@@ -326,10 +328,14 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
326
328
|
|
|
327
329
|
for (let i = 0; i < lines.length; i++) {
|
|
328
330
|
const line = lines[i];
|
|
331
|
+
const cleanLine = cleanLines[i] ?? "";
|
|
332
|
+
const cleanTrimmed = cleanLine.trim();
|
|
329
333
|
const trimmed = line.trim();
|
|
330
334
|
|
|
335
|
+
if (!cleanTrimmed) continue;
|
|
336
|
+
|
|
331
337
|
// Check group opens: Route::...group(
|
|
332
|
-
if (
|
|
338
|
+
if (cleanTrimmed.includes("Route::") && cleanTrimmed.includes("group(") && cleanTrimmed.includes("function")) {
|
|
333
339
|
const prefixMatch = /prefix\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(trimmed);
|
|
334
340
|
const controllerMatch = /controller\s*\(\s*([\\A-Za-z0-9_]+)::class\s*\)/.exec(trimmed);
|
|
335
341
|
const nameMatch = /name\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(trimmed);
|
|
@@ -351,8 +357,8 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
351
357
|
});
|
|
352
358
|
}
|
|
353
359
|
|
|
354
|
-
// Track brace depth
|
|
355
|
-
for (const char of
|
|
360
|
+
// Track brace depth on string/comment-stripped line so '/orders/{id}' never corrupts braceDepth
|
|
361
|
+
for (const char of cleanLine) {
|
|
356
362
|
if (char === "{") currentBraceDepth++;
|
|
357
363
|
if (char === "}") {
|
|
358
364
|
currentBraceDepth--;
|
|
@@ -366,14 +372,14 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
366
372
|
}
|
|
367
373
|
|
|
368
374
|
// Route statement accumulation across lines
|
|
369
|
-
if (!inStatement &&
|
|
375
|
+
if (!inStatement && cleanTrimmed.startsWith("Route::") && !cleanTrimmed.includes("Route::resource") && !cleanTrimmed.includes("Route::group") && !(cleanTrimmed.includes("group(") && cleanTrimmed.includes("function"))) {
|
|
370
376
|
statementBuffer = trimmed;
|
|
371
377
|
inStatement = true;
|
|
372
378
|
} else if (inStatement) {
|
|
373
379
|
statementBuffer += " " + trimmed;
|
|
374
380
|
}
|
|
375
381
|
|
|
376
|
-
if (inStatement &&
|
|
382
|
+
if (inStatement && cleanLine.includes(";")) {
|
|
377
383
|
inStatement = false;
|
|
378
384
|
const stmt = statementBuffer;
|
|
379
385
|
statementBuffer = "";
|
|
@@ -519,16 +525,17 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
519
525
|
}
|
|
520
526
|
|
|
521
527
|
private extractActionBody(controllerCode: string, actionName: string): string {
|
|
522
|
-
const
|
|
523
|
-
const
|
|
528
|
+
const cleanCode = blankCommentsAndStrings(controllerCode, { hashComments: true });
|
|
529
|
+
const fnRegex = new RegExp(`(?:public\\s+)?function\\s+${actionName}\\s*\\([^{]*\\)\\s*\\{`, "i");
|
|
530
|
+
const match = fnRegex.exec(cleanCode);
|
|
524
531
|
if (!match) return "";
|
|
525
532
|
|
|
526
533
|
const startIndex = match.index + match[0].length;
|
|
527
534
|
let braceDepth = 1;
|
|
528
535
|
let endIndex = startIndex;
|
|
529
536
|
|
|
530
|
-
while (braceDepth > 0 && endIndex <
|
|
531
|
-
const char =
|
|
537
|
+
while (braceDepth > 0 && endIndex < cleanCode.length) {
|
|
538
|
+
const char = cleanCode[endIndex];
|
|
532
539
|
if (char === "{") braceDepth++;
|
|
533
540
|
else if (char === "}") braceDepth--;
|
|
534
541
|
endIndex++;
|
|
@@ -750,10 +757,9 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
750
757
|
|
|
751
758
|
private extractValidationRules(requestCode: string): Record<string, string> {
|
|
752
759
|
const rules: Record<string, string> = {};
|
|
753
|
-
const
|
|
754
|
-
if (!
|
|
760
|
+
const body = this.extractActionBody(requestCode, "rules");
|
|
761
|
+
if (!body) return rules;
|
|
755
762
|
|
|
756
|
-
const body = rulesMatch[1];
|
|
757
763
|
// Match 'status' => 'in:pending,paid' or 'status' => ['required', 'string']
|
|
758
764
|
const rulePairRegex = /['"]([A-Za-z0-9_]+)['"]\s*=>\s*(?:['"]([^'"]+)['"]|\[([^\]]+)\])/g;
|
|
759
765
|
let rm;
|