septum 0.1.4 → 0.1.6
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/LICENSE +21 -21
- package/README.md +105 -105
- package/bin/septum.ts +4 -4
- package/package.json +63 -63
- package/src/cli/commands/check.ts +161 -161
- package/src/cli/commands/hook.ts +216 -188
- package/src/cli/commands/ingest.ts +29 -29
- package/src/cli/commands/init.ts +213 -211
- package/src/cli/commands/locate.ts +61 -61
- package/src/cli/commands/query.ts +104 -64
- package/src/cli/commands/serve.ts +5 -5
- package/src/cli/commands/slice.ts +50 -48
- package/src/cli/commands/sync.ts +31 -31
- package/src/cli/index.ts +220 -210
- package/src/cli/templates/hooks.ts +75 -75
- package/src/cli/templates/rules.ts +50 -38
- package/src/cli/templates/skill.ts +65 -69
- package/src/core/boundary/evaluator.ts +429 -210
- package/src/core/config/loader.ts +117 -97
- package/src/core/config/schema.ts +39 -39
- package/src/core/database/client.ts +44 -44
- package/src/core/database/repositories/dependency.repository.ts +552 -349
- package/src/core/database/repositories/domain.repository.ts +275 -198
- package/src/core/database/repositories/file.repository.ts +117 -106
- package/src/core/database/repositories/meta.repository.ts +33 -33
- package/src/core/database/repositories/symbol.repository.ts +482 -443
- package/src/core/database/repository.ts +281 -260
- package/src/core/database/schema.ts +154 -153
- package/src/core/discovery/topology-detector.ts +486 -486
- package/src/core/ingestion/hasher.ts +5 -5
- package/src/core/ingestion/pipeline.ts +606 -535
- package/src/core/parser/boundary-tracker.ts +433 -288
- package/src/core/parser/extractors/base.ts +62 -62
- package/src/core/parser/extractors/blade.ts +158 -158
- package/src/core/parser/extractors/frontend.ts +147 -194
- package/src/core/parser/extractors/go.ts +408 -180
- package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
- package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
- package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
- package/src/core/parser/extractors/php.ts +415 -153
- package/src/core/parser/extractors/python.ts +460 -228
- package/src/core/parser/extractors/registry.ts +70 -70
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
- package/src/core/parser/extractors/typescript.ts +668 -181
- package/src/core/parser/tree-sitter.ts +41 -41
- package/src/core/parser/wasm-loader.ts +271 -0
- package/src/core/resolver/call-graph-tracer.ts +698 -668
- package/src/core/resolver/module-resolver.ts +479 -342
- package/src/core/resolver/path-resolver.ts +147 -147
- package/src/core/resolver/symbol-locator.ts +543 -543
- package/src/core/resolver/vertical-slice-tracer.ts +323 -285
- package/src/core/session/session-manager.ts +56 -56
- package/src/core/telemetry/telemetry.ts +86 -86
- package/src/core/topology/environment-detector.ts +109 -6
- package/src/index.ts +21 -21
- package/src/mcp/schemas.ts +695 -286
- package/src/mcp/server.ts +927 -716
- package/src/mcp/tools/check-boundary.ts +307 -152
- package/src/mcp/tools/get-domain-catalog.ts +155 -121
- package/src/mcp/tools/get-environment-topology.ts +3 -6
- package/src/mcp/tools/get-feature-context.ts +227 -116
- package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
- package/src/mcp/tools/get-symbol-impact.ts +28 -26
- package/src/mcp/tools/get-symbol.ts +65 -63
- package/src/mcp/tools/locate-symbol.ts +169 -190
- package/src/mcp/tools/register-domain.ts +59 -64
- package/src/mcp/tools/register-feature.ts +91 -0
- package/src/mcp/tools/trace-vertical-slice.ts +74 -48
- package/src/types/index.ts +366 -315
- package/src/version.ts +3 -3
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import type { ArchetypeKind, ParsedFileAST } from "../../../types/index.ts";
|
|
2
|
-
|
|
3
|
-
export interface CodeExtractor {
|
|
4
|
-
canHandle(filePath: string): boolean;
|
|
5
|
-
extract(filePath: string, content: string): Promise<ParsedFileAST>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function detectArchetype(filePath: string, archetypesMap: Record<string, string>): ArchetypeKind {
|
|
9
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
10
|
-
|
|
11
|
-
// Check user-configured archetype glob patterns
|
|
12
|
-
for (const [archetype, pattern] of Object.entries(archetypesMap)) {
|
|
13
|
-
if (matchesSimpleGlob(normalized, pattern)) {
|
|
14
|
-
return archetype as ArchetypeKind;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Built-in convention heuristics
|
|
19
|
-
const lower = normalized.toLowerCase();
|
|
20
|
-
if (lower.includes("controller")) return "controller";
|
|
21
|
-
if (lower.includes("service")) return "service";
|
|
22
|
-
if (lower.includes("repository") || lower.includes("repo")) return "repository";
|
|
23
|
-
if (lower.includes("model") || lower.includes("entities") || lower.includes("entity")) return "model";
|
|
24
|
-
if (lower.includes("action")) return "action";
|
|
25
|
-
if (lower.includes("job") || lower.includes("queue")) return "job";
|
|
26
|
-
if (lower.includes("event")) return "event";
|
|
27
|
-
if (lower.includes("listener")) return "listener";
|
|
28
|
-
if (lower.includes("middleware")) return "middleware";
|
|
29
|
-
if (lower.includes("request")) return "request";
|
|
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";
|
|
33
|
-
if (lower.includes("util") || lower.includes("helper")) return "util";
|
|
34
|
-
|
|
35
|
-
return "unknown";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function matchesSimpleGlob(path: string, pattern: string): boolean {
|
|
39
|
-
const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
40
|
-
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
41
|
-
|
|
42
|
-
const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
43
|
-
const regexStr = escaped
|
|
44
|
-
.replace(/\*\*\//g, "(?:.*\\/)?")
|
|
45
|
-
.replace(/\*\*/g, ".*")
|
|
46
|
-
.replace(/\*/g, "[^/]*")
|
|
47
|
-
.replace(/\?/g, "[^/]");
|
|
48
|
-
|
|
49
|
-
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
50
|
-
if (regex.test(normalizedPath)) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// If pattern does not contain directory separators, allow matching against the basename
|
|
55
|
-
if (!normalizedPattern.includes("/")) {
|
|
56
|
-
const base = normalizedPath.split("/").pop() ?? "";
|
|
57
|
-
return regex.test(base);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
|
|
1
|
+
import type { ArchetypeKind, ParsedFileAST } from "../../../types/index.ts";
|
|
2
|
+
|
|
3
|
+
export interface CodeExtractor {
|
|
4
|
+
canHandle(filePath: string): boolean;
|
|
5
|
+
extract(filePath: string, content: string): Promise<ParsedFileAST>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function detectArchetype(filePath: string, archetypesMap: Record<string, string>): ArchetypeKind {
|
|
9
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
10
|
+
|
|
11
|
+
// Check user-configured archetype glob patterns
|
|
12
|
+
for (const [archetype, pattern] of Object.entries(archetypesMap)) {
|
|
13
|
+
if (matchesSimpleGlob(normalized, pattern)) {
|
|
14
|
+
return archetype as ArchetypeKind;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Built-in convention heuristics
|
|
19
|
+
const lower = normalized.toLowerCase();
|
|
20
|
+
if (lower.includes("controller")) return "controller";
|
|
21
|
+
if (lower.includes("service")) return "service";
|
|
22
|
+
if (lower.includes("repository") || lower.includes("repo")) return "repository";
|
|
23
|
+
if (lower.includes("model") || lower.includes("entities") || lower.includes("entity")) return "model";
|
|
24
|
+
if (lower.includes("action")) return "action";
|
|
25
|
+
if (lower.includes("job") || lower.includes("queue")) return "job";
|
|
26
|
+
if (lower.includes("event")) return "event";
|
|
27
|
+
if (lower.includes("listener")) return "listener";
|
|
28
|
+
if (lower.includes("middleware")) return "middleware";
|
|
29
|
+
if (lower.includes("request")) return "request";
|
|
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";
|
|
33
|
+
if (lower.includes("util") || lower.includes("helper")) return "util";
|
|
34
|
+
|
|
35
|
+
return "unknown";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function matchesSimpleGlob(path: string, pattern: string): boolean {
|
|
39
|
+
const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
40
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
41
|
+
|
|
42
|
+
const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
43
|
+
const regexStr = escaped
|
|
44
|
+
.replace(/\*\*\//g, "(?:.*\\/)?")
|
|
45
|
+
.replace(/\*\*/g, ".*")
|
|
46
|
+
.replace(/\*/g, "[^/]*")
|
|
47
|
+
.replace(/\?/g, "[^/]");
|
|
48
|
+
|
|
49
|
+
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
50
|
+
if (regex.test(normalizedPath)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If pattern does not contain directory separators, allow matching against the basename
|
|
55
|
+
if (!normalizedPattern.includes("/")) {
|
|
56
|
+
const base = normalizedPath.split("/").pop() ?? "";
|
|
57
|
+
return regex.test(base);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
@@ -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
|
+
}
|