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,6 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { VerticalSliceRecord } from "../../../types/index.ts";
|
|
3
|
+
import type { ExecutionChainNode, VerticalSliceRecord } from "../../../types/index.ts";
|
|
4
|
+
import { BehavioralAnalyzer } from "../behavioral-analyzer.ts";
|
|
5
|
+
import { SchemaProviderRegistry } from "../schema/schema-provider-registry.ts";
|
|
4
6
|
import type {
|
|
5
7
|
SemanticSliceExtractor,
|
|
6
8
|
VerticalSliceCandidate,
|
|
@@ -12,6 +14,7 @@ export interface ParsedRoute {
|
|
|
12
14
|
routeName?: string;
|
|
13
15
|
controllerClass: string;
|
|
14
16
|
actionName: string;
|
|
17
|
+
file?: string;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export type LaravelSemanticExtractionResult = VerticalSliceCandidate[] & {
|
|
@@ -85,9 +88,11 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
85
88
|
props: string[];
|
|
86
89
|
} | null = null;
|
|
87
90
|
|
|
91
|
+
let behavioralInfo: import("../behavioral-analyzer.ts").MethodBehavioralSummary | null = null;
|
|
88
92
|
if (controllerInfo.filePath && fs.existsSync(controllerInfo.filePath)) {
|
|
89
93
|
const controllerCode = fs.readFileSync(controllerInfo.filePath, "utf-8");
|
|
90
94
|
const actionBody = this.extractActionBody(controllerCode, route.actionName);
|
|
95
|
+
behavioralInfo = BehavioralAnalyzer.analyzeMethod(actionBody);
|
|
91
96
|
|
|
92
97
|
// 1. Detect FormRequest in method signature
|
|
93
98
|
const requestClassName = this.detectFormRequestClass(controllerCode, route.actionName);
|
|
@@ -118,7 +123,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
118
123
|
frontendInfo = this.detectFrontendTarget(actionBody);
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
const chain:
|
|
126
|
+
const chain: ExecutionChainNode[] = [
|
|
122
127
|
{
|
|
123
128
|
stage: "ingress",
|
|
124
129
|
symbol: `${route.httpMethod.toUpperCase()} ${route.uri.startsWith("/") ? route.uri : `/${route.uri}`}`,
|
|
@@ -127,6 +132,15 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
127
132
|
},
|
|
128
133
|
];
|
|
129
134
|
|
|
135
|
+
if (behavioralInfo && behavioralInfo.guards.length > 0) {
|
|
136
|
+
chain.push({
|
|
137
|
+
stage: "guard",
|
|
138
|
+
symbol: behavioralInfo.guards.join(", "),
|
|
139
|
+
description: "Authorization / Security Guard",
|
|
140
|
+
guards: behavioralInfo.guards,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
130
144
|
if (formRequestInfo) {
|
|
131
145
|
chain.push({
|
|
132
146
|
stage: "validation",
|
|
@@ -142,17 +156,70 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
142
156
|
file: controllerInfo.filePath ? path.relative(this.projectRoot, controllerInfo.filePath) : undefined,
|
|
143
157
|
line: controllerInfo.line,
|
|
144
158
|
description: `Controller Action`,
|
|
159
|
+
guards: behavioralInfo && behavioralInfo.guards.length > 0 ? behavioralInfo.guards : undefined,
|
|
160
|
+
mutations: behavioralInfo && behavioralInfo.mutations.length > 0 ? behavioralInfo.mutations : undefined,
|
|
161
|
+
transactions: behavioralInfo?.hasTransaction,
|
|
145
162
|
});
|
|
146
163
|
|
|
164
|
+
if (behavioralInfo && behavioralInfo.hasTransaction) {
|
|
165
|
+
chain.push({
|
|
166
|
+
stage: "transaction",
|
|
167
|
+
symbol: "DB::transaction",
|
|
168
|
+
description: "Database Transaction Scope",
|
|
169
|
+
transactions: true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
147
173
|
if (modelInfo) {
|
|
174
|
+
const schema = SchemaProviderRegistry.resolveSchema(
|
|
175
|
+
root,
|
|
176
|
+
modelInfo.className,
|
|
177
|
+
modelInfo.filePath
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const columnsSummary =
|
|
181
|
+
schema && schema.fields.length > 0
|
|
182
|
+
? schema.fields.map((f) => `${f.name}:${f.type}${f.nullable ? "?" : ""}`).join(", ")
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
148
185
|
chain.push({
|
|
149
186
|
stage: "entity",
|
|
150
187
|
symbol: modelInfo.className,
|
|
151
188
|
file: modelInfo.filePath,
|
|
152
|
-
description: "Eloquent Model",
|
|
189
|
+
description: schema?.tableName ? `Eloquent Model (${schema.tableName})` : "Eloquent Model",
|
|
190
|
+
table: schema?.tableName,
|
|
191
|
+
schema_file: schema?.schemaSourceFile,
|
|
192
|
+
columns_summary: columnsSummary,
|
|
193
|
+
mutations: behavioralInfo && behavioralInfo.mutations.length > 0 ? behavioralInfo.mutations : undefined,
|
|
153
194
|
});
|
|
154
195
|
}
|
|
155
196
|
|
|
197
|
+
if (behavioralInfo && behavioralInfo.dispatchedJobs.length > 0) {
|
|
198
|
+
for (const job of behavioralInfo.dispatchedJobs) {
|
|
199
|
+
const jobFile = this.findClassFile("Jobs", job);
|
|
200
|
+
chain.push({
|
|
201
|
+
stage: "job",
|
|
202
|
+
symbol: job,
|
|
203
|
+
file: jobFile ? path.relative(this.projectRoot, jobFile) : undefined,
|
|
204
|
+
description: "Background Queue Job Dispatched",
|
|
205
|
+
dispatches: [job],
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (behavioralInfo && behavioralInfo.emittedEvents.length > 0) {
|
|
211
|
+
for (const evt of behavioralInfo.emittedEvents) {
|
|
212
|
+
const evtFile = this.findClassFile("Events", evt);
|
|
213
|
+
chain.push({
|
|
214
|
+
stage: "event",
|
|
215
|
+
symbol: evt,
|
|
216
|
+
file: evtFile ? path.relative(this.projectRoot, evtFile) : undefined,
|
|
217
|
+
description: "Domain Event Emitted",
|
|
218
|
+
emits: [evt],
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
156
223
|
if (frontendInfo) {
|
|
157
224
|
chain.push({
|
|
158
225
|
stage: "egress",
|
|
@@ -177,6 +244,11 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
177
244
|
});
|
|
178
245
|
}
|
|
179
246
|
|
|
247
|
+
// Also discover Omni-Trigger slices (Jobs, Console Commands, Event Listeners)
|
|
248
|
+
slices.push(...this.extractJobSlices(root, domainId));
|
|
249
|
+
slices.push(...this.extractCommandSlices(root, domainId));
|
|
250
|
+
slices.push(...this.extractListenerSlices(root, domainId));
|
|
251
|
+
|
|
180
252
|
return Object.assign(slices, { slices });
|
|
181
253
|
}
|
|
182
254
|
|
|
@@ -524,7 +596,10 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
524
596
|
return null;
|
|
525
597
|
}
|
|
526
598
|
|
|
527
|
-
private findClassFile(
|
|
599
|
+
private findClassFile(
|
|
600
|
+
subDir: "Requests" | "Models" | "Jobs" | "Events" | "Listeners" | "Console/Commands",
|
|
601
|
+
className: string
|
|
602
|
+
): string | null {
|
|
528
603
|
const directPath = path.join(this.projectRoot, `app/${subDir}/${className}.php`);
|
|
529
604
|
if (fs.existsSync(directPath)) return directPath;
|
|
530
605
|
|
|
@@ -582,4 +657,245 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
|
|
|
582
657
|
|
|
583
658
|
return { fillable, casts };
|
|
584
659
|
}
|
|
660
|
+
|
|
661
|
+
private extractJobSlices(
|
|
662
|
+
root: string,
|
|
663
|
+
domainId?: number
|
|
664
|
+
): Array<Omit<VerticalSliceRecord, "id" | "created_at">> {
|
|
665
|
+
const jobsDir = path.join(root, "app/Jobs");
|
|
666
|
+
if (!fs.existsSync(jobsDir)) return [];
|
|
667
|
+
|
|
668
|
+
const jobFiles = fs.readdirSync(jobsDir).filter((f) => f.endsWith(".php"));
|
|
669
|
+
const slices: Array<Omit<VerticalSliceRecord, "id" | "created_at">> = [];
|
|
670
|
+
|
|
671
|
+
for (const file of jobFiles) {
|
|
672
|
+
const fullPath = path.join(jobsDir, file);
|
|
673
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
674
|
+
const classMatch = content.match(/class\s+([A-Za-z0-9_]+)/);
|
|
675
|
+
if (!classMatch) continue;
|
|
676
|
+
|
|
677
|
+
const className = classMatch[1];
|
|
678
|
+
const actionBody = this.extractActionBody(content, "handle");
|
|
679
|
+
const behavior = BehavioralAnalyzer.analyzeMethod(actionBody);
|
|
680
|
+
|
|
681
|
+
const chain: ExecutionChainNode[] = [
|
|
682
|
+
{
|
|
683
|
+
stage: "ingress",
|
|
684
|
+
symbol: `JOB ${className}`,
|
|
685
|
+
file: path.relative(this.projectRoot, fullPath),
|
|
686
|
+
description: `Queue Worker Trigger: ${className}`,
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
stage: "controller",
|
|
690
|
+
symbol: `${className}::handle`,
|
|
691
|
+
file: path.relative(this.projectRoot, fullPath),
|
|
692
|
+
description: "Queue Job Handler",
|
|
693
|
+
mutations: behavior.mutations.length ? behavior.mutations : undefined,
|
|
694
|
+
transactions: behavior.hasTransaction,
|
|
695
|
+
},
|
|
696
|
+
];
|
|
697
|
+
|
|
698
|
+
if (behavior.hasTransaction) {
|
|
699
|
+
chain.push({
|
|
700
|
+
stage: "transaction",
|
|
701
|
+
symbol: "DB::transaction",
|
|
702
|
+
description: "Database Transaction Scope",
|
|
703
|
+
transactions: true,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
for (const evt of behavior.emittedEvents) {
|
|
708
|
+
const evtFile = this.findClassFile("Events", evt);
|
|
709
|
+
chain.push({
|
|
710
|
+
stage: "event",
|
|
711
|
+
symbol: evt,
|
|
712
|
+
file: evtFile ? path.relative(this.projectRoot, evtFile) : undefined,
|
|
713
|
+
description: "Domain Event Emitted",
|
|
714
|
+
emits: [evt],
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
slices.push({
|
|
719
|
+
domain_id: domainId ?? null,
|
|
720
|
+
feature_key: `job:${className}`,
|
|
721
|
+
http_method: "JOB",
|
|
722
|
+
route_uri: `job:${className}`,
|
|
723
|
+
route_name: `queue.${this.toSnakeCase(className)}`,
|
|
724
|
+
controller_class: className,
|
|
725
|
+
action_name: "handle",
|
|
726
|
+
controller_file: path.relative(this.projectRoot, fullPath),
|
|
727
|
+
controller_line: 1,
|
|
728
|
+
architecture_style: "clean",
|
|
729
|
+
entry_kind: "queue_job",
|
|
730
|
+
execution_chain_json: JSON.stringify(chain),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return slices;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private extractCommandSlices(
|
|
738
|
+
root: string,
|
|
739
|
+
domainId?: number
|
|
740
|
+
): Array<Omit<VerticalSliceRecord, "id" | "created_at">> {
|
|
741
|
+
const commandsDir = path.join(root, "app/Console/Commands");
|
|
742
|
+
if (!fs.existsSync(commandsDir)) return [];
|
|
743
|
+
|
|
744
|
+
const files = fs.readdirSync(commandsDir).filter((f) => f.endsWith(".php"));
|
|
745
|
+
const slices: Array<Omit<VerticalSliceRecord, "id" | "created_at">> = [];
|
|
746
|
+
|
|
747
|
+
for (const file of files) {
|
|
748
|
+
const fullPath = path.join(commandsDir, file);
|
|
749
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
750
|
+
const classMatch = content.match(/class\s+([A-Za-z0-9_]+)/);
|
|
751
|
+
if (!classMatch) continue;
|
|
752
|
+
|
|
753
|
+
const className = classMatch[1];
|
|
754
|
+
const sigMatch = content.match(/protected\s+\$signature\s*=\s*['"]([^'"]+)['"]/);
|
|
755
|
+
const commandSignature = sigMatch ? sigMatch[1].split(/\s+/)[0] : this.toSnakeCase(className);
|
|
756
|
+
|
|
757
|
+
const actionBody = this.extractActionBody(content, "handle");
|
|
758
|
+
const behavior = BehavioralAnalyzer.analyzeMethod(actionBody);
|
|
759
|
+
|
|
760
|
+
const chain: ExecutionChainNode[] = [
|
|
761
|
+
{
|
|
762
|
+
stage: "ingress",
|
|
763
|
+
symbol: `CLI ${commandSignature}`,
|
|
764
|
+
file: path.relative(this.projectRoot, fullPath),
|
|
765
|
+
description: `Console Command: ${commandSignature}`,
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
stage: "controller",
|
|
769
|
+
symbol: `${className}::handle`,
|
|
770
|
+
file: path.relative(this.projectRoot, fullPath),
|
|
771
|
+
description: "Command Execution Handler",
|
|
772
|
+
mutations: behavior.mutations.length ? behavior.mutations : undefined,
|
|
773
|
+
transactions: behavior.hasTransaction,
|
|
774
|
+
},
|
|
775
|
+
];
|
|
776
|
+
|
|
777
|
+
if (behavior.hasTransaction) {
|
|
778
|
+
chain.push({
|
|
779
|
+
stage: "transaction",
|
|
780
|
+
symbol: "DB::transaction",
|
|
781
|
+
description: "Database Transaction Scope",
|
|
782
|
+
transactions: true,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
for (const job of behavior.dispatchedJobs) {
|
|
787
|
+
const jobFile = this.findClassFile("Jobs", job);
|
|
788
|
+
chain.push({
|
|
789
|
+
stage: "job",
|
|
790
|
+
symbol: job,
|
|
791
|
+
file: jobFile ? path.relative(this.projectRoot, jobFile) : undefined,
|
|
792
|
+
description: "Dispatched Background Worker",
|
|
793
|
+
dispatches: [job],
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
slices.push({
|
|
798
|
+
domain_id: domainId ?? null,
|
|
799
|
+
feature_key: `command:${commandSignature}`,
|
|
800
|
+
http_method: "CLI",
|
|
801
|
+
route_uri: `command:${commandSignature}`,
|
|
802
|
+
route_name: `cli.${commandSignature}`,
|
|
803
|
+
controller_class: className,
|
|
804
|
+
action_name: "handle",
|
|
805
|
+
controller_file: path.relative(this.projectRoot, fullPath),
|
|
806
|
+
controller_line: 1,
|
|
807
|
+
architecture_style: "clean",
|
|
808
|
+
entry_kind: "scheduled_command",
|
|
809
|
+
execution_chain_json: JSON.stringify(chain),
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return slices;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
private extractListenerSlices(
|
|
817
|
+
root: string,
|
|
818
|
+
domainId?: number
|
|
819
|
+
): Array<Omit<VerticalSliceRecord, "id" | "created_at">> {
|
|
820
|
+
const listenersDir = path.join(root, "app/Listeners");
|
|
821
|
+
if (!fs.existsSync(listenersDir)) return [];
|
|
822
|
+
|
|
823
|
+
const files = fs.readdirSync(listenersDir).filter((f) => f.endsWith(".php"));
|
|
824
|
+
const slices: Array<Omit<VerticalSliceRecord, "id" | "created_at">> = [];
|
|
825
|
+
|
|
826
|
+
for (const file of files) {
|
|
827
|
+
const fullPath = path.join(listenersDir, file);
|
|
828
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
829
|
+
const classMatch = content.match(/class\s+([A-Za-z0-9_]+)/);
|
|
830
|
+
if (!classMatch) continue;
|
|
831
|
+
|
|
832
|
+
const className = classMatch[1];
|
|
833
|
+
const actionBody = this.extractActionBody(content, "handle");
|
|
834
|
+
const behavior = BehavioralAnalyzer.analyzeMethod(actionBody);
|
|
835
|
+
|
|
836
|
+
const eventParamMatch = content.match(/public\s+function\s+handle\s*\(\s*([A-Za-z0-9_]+)\s+\$event\s*\)/i);
|
|
837
|
+
const listenedEvent = eventParamMatch ? eventParamMatch[1] : undefined;
|
|
838
|
+
|
|
839
|
+
const chain: ExecutionChainNode[] = [
|
|
840
|
+
{
|
|
841
|
+
stage: "ingress",
|
|
842
|
+
symbol: `EVENT ${listenedEvent || className}`,
|
|
843
|
+
file: path.relative(this.projectRoot, fullPath),
|
|
844
|
+
description: `Event Listener Trigger${listenedEvent ? ` for [${listenedEvent}]` : ""}`,
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
stage: "controller",
|
|
848
|
+
symbol: `${className}::handle`,
|
|
849
|
+
file: path.relative(this.projectRoot, fullPath),
|
|
850
|
+
description: "Event Listener Handler",
|
|
851
|
+
mutations: behavior.mutations.length ? behavior.mutations : undefined,
|
|
852
|
+
transactions: behavior.hasTransaction,
|
|
853
|
+
},
|
|
854
|
+
];
|
|
855
|
+
|
|
856
|
+
if (behavior.hasTransaction) {
|
|
857
|
+
chain.push({
|
|
858
|
+
stage: "transaction",
|
|
859
|
+
symbol: "DB::transaction",
|
|
860
|
+
description: "Database Transaction Scope",
|
|
861
|
+
transactions: true,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
for (const job of behavior.dispatchedJobs) {
|
|
866
|
+
const jobFile = this.findClassFile("Jobs", job);
|
|
867
|
+
chain.push({
|
|
868
|
+
stage: "job",
|
|
869
|
+
symbol: job,
|
|
870
|
+
file: jobFile ? path.relative(this.projectRoot, jobFile) : undefined,
|
|
871
|
+
description: "Dispatched Background Worker",
|
|
872
|
+
dispatches: [job],
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
slices.push({
|
|
877
|
+
domain_id: domainId ?? null,
|
|
878
|
+
feature_key: `listener:${className}`,
|
|
879
|
+
http_method: "EVENT",
|
|
880
|
+
route_uri: `event:${className}`,
|
|
881
|
+
route_name: `event.${this.toSnakeCase(className)}`,
|
|
882
|
+
controller_class: className,
|
|
883
|
+
action_name: "handle",
|
|
884
|
+
controller_file: path.relative(this.projectRoot, fullPath),
|
|
885
|
+
controller_line: 1,
|
|
886
|
+
architecture_style: "clean",
|
|
887
|
+
entry_kind: "event_listener",
|
|
888
|
+
execution_chain_json: JSON.stringify(chain),
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return slices;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
private toSnakeCase(str: string): string {
|
|
896
|
+
return str
|
|
897
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
898
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
899
|
+
.toLowerCase();
|
|
900
|
+
}
|
|
585
901
|
}
|
|
@@ -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
|
export class PHPExtractor implements CodeExtractor {
|
|
@@ -112,6 +112,7 @@ export class PHPExtractor implements CodeExtractor {
|
|
|
112
112
|
const params = methodMatch[3].trim();
|
|
113
113
|
const retType = methodMatch[4] ? `: ${methodMatch[4].trim()}` : "";
|
|
114
114
|
const endLine = findBraceBlockEnd(lines, i);
|
|
115
|
+
const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
|
|
115
116
|
symbols.push({
|
|
116
117
|
name: `${currentContainer}::${name}`,
|
|
117
118
|
kind: "method",
|
|
@@ -120,6 +121,7 @@ export class PHPExtractor implements CodeExtractor {
|
|
|
120
121
|
line_start: lineNum,
|
|
121
122
|
line_end: endLine,
|
|
122
123
|
line_count: Math.max(1, endLine - lineNum + 1),
|
|
124
|
+
nesting_depth: nestingDepth,
|
|
123
125
|
});
|
|
124
126
|
continue;
|
|
125
127
|
}
|
|
@@ -132,6 +134,7 @@ export class PHPExtractor implements CodeExtractor {
|
|
|
132
134
|
const params = funcMatch[2].trim();
|
|
133
135
|
const retType = funcMatch[3] ? `: ${funcMatch[3].trim()}` : "";
|
|
134
136
|
const endLine = findBraceBlockEnd(lines, i);
|
|
137
|
+
const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
|
|
135
138
|
symbols.push({
|
|
136
139
|
name,
|
|
137
140
|
kind: "function",
|
|
@@ -140,6 +143,7 @@ export class PHPExtractor implements CodeExtractor {
|
|
|
140
143
|
line_start: lineNum,
|
|
141
144
|
line_end: endLine,
|
|
142
145
|
line_count: Math.max(1, endLine - lineNum + 1),
|
|
146
|
+
nesting_depth: nestingDepth,
|
|
143
147
|
});
|
|
144
148
|
}
|
|
145
149
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtractedDependency, ExtractedSymbol, ParsedFileAST, Visibility } from "../../../types/index.ts";
|
|
2
|
-
import { findPythonBlockEnd } from "../boundary-tracker.ts";
|
|
2
|
+
import { findPythonBlockEnd, calculatePythonNesting } from "../boundary-tracker.ts";
|
|
3
3
|
import type { CodeExtractor } from "./base.ts";
|
|
4
4
|
|
|
5
5
|
const PYTHON_STDLIB_AND_EXTERNALS = new Set([
|
|
@@ -206,6 +206,7 @@ export class PythonExtractor implements CodeExtractor {
|
|
|
206
206
|
: "public";
|
|
207
207
|
|
|
208
208
|
const endLine = findPythonBlockEnd(lines, i);
|
|
209
|
+
const nestingDepth = calculatePythonNesting(lines, i, endLine - 1);
|
|
209
210
|
symbols.push({
|
|
210
211
|
name: isMethod ? `${currentClass}::${funcName}` : funcName,
|
|
211
212
|
kind: isMethod ? "method" : "function",
|
|
@@ -214,6 +215,7 @@ export class PythonExtractor implements CodeExtractor {
|
|
|
214
215
|
line_start: lineNum,
|
|
215
216
|
line_end: endLine,
|
|
216
217
|
line_count: Math.max(1, endLine - lineNum + 1),
|
|
218
|
+
nesting_depth: nestingDepth,
|
|
217
219
|
});
|
|
218
220
|
}
|
|
219
221
|
}
|
|
@@ -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
|
export class TypeScriptExtractor implements CodeExtractor {
|
|
@@ -103,6 +103,7 @@ export class TypeScriptExtractor implements CodeExtractor {
|
|
|
103
103
|
const params = fnMatch[2];
|
|
104
104
|
const retType = fnMatch[3] ? `: ${fnMatch[3].trim()}` : "";
|
|
105
105
|
const endLine = findBraceBlockEnd(lines, i);
|
|
106
|
+
const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
|
|
106
107
|
symbols.push({
|
|
107
108
|
name,
|
|
108
109
|
kind: "function",
|
|
@@ -111,6 +112,7 @@ export class TypeScriptExtractor implements CodeExtractor {
|
|
|
111
112
|
line_start: lineNum,
|
|
112
113
|
line_end: endLine,
|
|
113
114
|
line_count: Math.max(1, endLine - lineNum + 1),
|
|
115
|
+
nesting_depth: nestingDepth,
|
|
114
116
|
});
|
|
115
117
|
continue;
|
|
116
118
|
}
|
|
@@ -159,6 +161,7 @@ export class TypeScriptExtractor implements CodeExtractor {
|
|
|
159
161
|
const params = methodMatch[3];
|
|
160
162
|
const retType = methodMatch[4] ? `: ${methodMatch[4].trim()}` : "";
|
|
161
163
|
const endLine = findBraceBlockEnd(lines, i);
|
|
164
|
+
const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
|
|
162
165
|
symbols.push({
|
|
163
166
|
name: `${currentClass}::${name}`,
|
|
164
167
|
kind: "method",
|
|
@@ -167,6 +170,7 @@ export class TypeScriptExtractor implements CodeExtractor {
|
|
|
167
170
|
line_start: lineNum,
|
|
168
171
|
line_end: endLine,
|
|
169
172
|
line_count: Math.max(1, endLine - lineNum + 1),
|
|
173
|
+
nesting_depth: nestingDepth,
|
|
170
174
|
});
|
|
171
175
|
}
|
|
172
176
|
}
|