septum 0.1.0 → 0.1.2
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 +100 -100
- package/bin/septum.ts +4 -4
- package/package.json +63 -62
- package/src/cli/commands/check.ts +161 -161
- package/src/cli/commands/hook.ts +188 -188
- package/src/cli/commands/ingest.ts +29 -29
- package/src/cli/commands/init.ts +211 -211
- package/src/cli/commands/locate.ts +61 -61
- package/src/cli/commands/query.ts +64 -64
- package/src/cli/commands/serve.ts +5 -5
- package/src/cli/commands/slice.ts +90 -90
- package/src/cli/commands/sync.ts +31 -31
- package/src/cli/index.ts +199 -178
- package/src/cli/templates/hooks.ts +75 -75
- package/src/cli/templates/rules.ts +38 -38
- package/src/cli/templates/skill.ts +66 -66
- package/src/core/boundary/evaluator.ts +210 -210
- package/src/core/config/loader.ts +84 -84
- package/src/core/config/schema.ts +39 -39
- package/src/core/database/client.ts +44 -132
- package/src/core/database/repositories/dependency.repository.ts +279 -279
- package/src/core/database/repositories/domain.repository.ts +198 -198
- package/src/core/database/repositories/file.repository.ts +99 -99
- package/src/core/database/repositories/meta.repository.ts +33 -33
- package/src/core/database/repositories/symbol.repository.ts +424 -424
- package/src/core/database/repository.ts +242 -242
- package/src/core/database/schema.ts +120 -0
- package/src/core/discovery/topology-detector.ts +418 -418
- package/src/core/ingestion/hasher.ts +5 -5
- package/src/core/ingestion/pipeline.ts +438 -438
- package/src/core/parser/boundary-tracker.ts +128 -128
- package/src/core/parser/extractors/base.ts +60 -60
- package/src/core/parser/extractors/frontend.ts +194 -194
- package/src/core/parser/extractors/go.ts +176 -176
- package/src/core/parser/extractors/laravel-semantic.ts +426 -426
- package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -199
- package/src/core/parser/extractors/nestjs-semantic.ts +119 -119
- package/src/core/parser/extractors/php.ts +149 -149
- package/src/core/parser/extractors/python.ts +226 -226
- package/src/core/parser/extractors/registry.ts +49 -49
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
- package/src/core/parser/extractors/typescript.ts +177 -177
- package/src/core/parser/tree-sitter.ts +39 -39
- package/src/core/resolver/call-graph-tracer.ts +668 -668
- package/src/core/resolver/module-resolver.ts +342 -323
- package/src/core/resolver/symbol-locator.ts +543 -405
- package/src/core/resolver/vertical-slice-tracer.ts +237 -236
- package/src/core/session/session-manager.ts +56 -56
- package/src/core/telemetry/telemetry.ts +86 -86
- package/src/index.ts +18 -18
- package/src/mcp/schemas.ts +68 -68
- package/src/mcp/server.ts +612 -602
- package/src/mcp/tools/check-boundary.ts +102 -102
- package/src/mcp/tools/get-domain-catalog.ts +91 -91
- package/src/mcp/tools/get-feature-context.ts +116 -116
- package/src/mcp/tools/get-symbol-hotspots.ts +49 -49
- package/src/mcp/tools/get-symbol-impact.ts +26 -26
- package/src/mcp/tools/get-symbol.ts +63 -63
- package/src/mcp/tools/locate-symbol.ts +74 -74
- package/src/mcp/tools/register-domain.ts +64 -64
- package/src/mcp/tools/trace-vertical-slice.ts +102 -102
- package/src/types/index.ts +296 -296
- package/src/version.ts +3 -0
- package/src/core/database/schema.sql +0 -92
|
@@ -1,279 +1,279 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import type { Database } from "bun:sqlite";
|
|
3
|
-
import type {
|
|
4
|
-
ExtractedDependency,
|
|
5
|
-
GetSymbolImpactResponse,
|
|
6
|
-
InboundCaller,
|
|
7
|
-
VerticalSliceRecord,
|
|
8
|
-
} from "../../../types/index.ts";
|
|
9
|
-
|
|
10
|
-
export class DependencyRepository {
|
|
11
|
-
constructor(private db: Database) {}
|
|
12
|
-
|
|
13
|
-
public replaceFileDependencies(fileId: number, deps: ExtractedDependency[]): void {
|
|
14
|
-
this.db.query("DELETE FROM dependencies WHERE source_file_id = ?").run(fileId);
|
|
15
|
-
if (deps.length === 0) return;
|
|
16
|
-
|
|
17
|
-
const stmt = this.db.query(
|
|
18
|
-
`INSERT INTO dependencies (source_file_id, target_symbol_or_path, import_statement, line_number, is_external)
|
|
19
|
-
VALUES (?, ?, ?, ?, ?)`
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
for (const dep of deps) {
|
|
23
|
-
stmt.run(fileId, dep.target, dep.statement, dep.line_number, dep.is_external ? 1 : 0);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public upsertVerticalSlice(
|
|
28
|
-
slice: Omit<VerticalSliceRecord, "id" | "created_at">
|
|
29
|
-
): void {
|
|
30
|
-
const existing = this.db
|
|
31
|
-
.query<VerticalSliceRecord, [string, string]>(
|
|
32
|
-
"SELECT id FROM vertical_slices WHERE http_method = ? AND route_uri = ?"
|
|
33
|
-
)
|
|
34
|
-
.get(slice.http_method, slice.route_uri);
|
|
35
|
-
|
|
36
|
-
if (existing) {
|
|
37
|
-
this.db
|
|
38
|
-
.query(
|
|
39
|
-
`UPDATE vertical_slices
|
|
40
|
-
SET domain_id = ?, feature_key = ?, route_name = ?, controller_class = ?,
|
|
41
|
-
action_name = ?, controller_file = ?, controller_line = ?,
|
|
42
|
-
architecture_style = ?, entry_kind = ?, execution_chain_json = ?
|
|
43
|
-
WHERE id = ?`
|
|
44
|
-
)
|
|
45
|
-
.run(
|
|
46
|
-
slice.domain_id,
|
|
47
|
-
slice.feature_key,
|
|
48
|
-
slice.route_name,
|
|
49
|
-
slice.controller_class,
|
|
50
|
-
slice.action_name,
|
|
51
|
-
slice.controller_file,
|
|
52
|
-
slice.controller_line,
|
|
53
|
-
slice.architecture_style ?? "clean",
|
|
54
|
-
slice.entry_kind ?? "http_route",
|
|
55
|
-
slice.execution_chain_json,
|
|
56
|
-
existing.id
|
|
57
|
-
);
|
|
58
|
-
} else {
|
|
59
|
-
this.db
|
|
60
|
-
.query(
|
|
61
|
-
`INSERT INTO vertical_slices (
|
|
62
|
-
domain_id, feature_key, http_method, route_uri, route_name,
|
|
63
|
-
controller_class, action_name, controller_file, controller_line,
|
|
64
|
-
architecture_style, entry_kind, execution_chain_json
|
|
65
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
66
|
-
)
|
|
67
|
-
.run(
|
|
68
|
-
slice.domain_id,
|
|
69
|
-
slice.feature_key,
|
|
70
|
-
slice.http_method,
|
|
71
|
-
slice.route_uri,
|
|
72
|
-
slice.route_name,
|
|
73
|
-
slice.controller_class,
|
|
74
|
-
slice.action_name,
|
|
75
|
-
slice.controller_file,
|
|
76
|
-
slice.controller_line,
|
|
77
|
-
slice.architecture_style ?? "clean",
|
|
78
|
-
slice.entry_kind ?? "http_route",
|
|
79
|
-
slice.execution_chain_json
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
public findVerticalSlices(searchTerm: string): VerticalSliceRecord[] {
|
|
85
|
-
const term = searchTerm.trim();
|
|
86
|
-
const wild = `%${term}%`;
|
|
87
|
-
const query = `
|
|
88
|
-
SELECT * FROM vertical_slices
|
|
89
|
-
WHERE route_uri LIKE ?
|
|
90
|
-
OR route_name LIKE ?
|
|
91
|
-
OR controller_class LIKE ?
|
|
92
|
-
OR action_name LIKE ?
|
|
93
|
-
OR feature_key LIKE ?
|
|
94
|
-
OR execution_chain_json LIKE ?
|
|
95
|
-
ORDER BY id ASC
|
|
96
|
-
`;
|
|
97
|
-
return this.db.query<VerticalSliceRecord, [string, string, string, string, string, string]>(query).all(
|
|
98
|
-
wild, wild, wild, wild, wild, wild
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
public getAllVerticalSlices(): VerticalSliceRecord[] {
|
|
103
|
-
return this.db.query<VerticalSliceRecord, []>("SELECT * FROM vertical_slices ORDER BY id ASC").all();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
public clearVerticalSlices(domainId?: number): void {
|
|
107
|
-
if (domainId) {
|
|
108
|
-
this.db.query("DELETE FROM vertical_slices WHERE domain_id = ?").run(domainId);
|
|
109
|
-
} else {
|
|
110
|
-
this.db.query("DELETE FROM vertical_slices").run();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
public getAllDependenciesWithDomains(): Array<{
|
|
115
|
-
source_file: string;
|
|
116
|
-
source_domain: string;
|
|
117
|
-
line_number: number;
|
|
118
|
-
target: string;
|
|
119
|
-
statement: string;
|
|
120
|
-
is_external: boolean;
|
|
121
|
-
}> {
|
|
122
|
-
const query = `
|
|
123
|
-
SELECT
|
|
124
|
-
f.path AS source_file,
|
|
125
|
-
d.name AS source_domain,
|
|
126
|
-
dep.line_number,
|
|
127
|
-
dep.target_symbol_or_path AS target,
|
|
128
|
-
dep.import_statement AS statement,
|
|
129
|
-
dep.is_external
|
|
130
|
-
FROM dependencies dep
|
|
131
|
-
JOIN files f ON dep.source_file_id = f.id
|
|
132
|
-
JOIN domains d ON f.domain_id = d.id
|
|
133
|
-
ORDER BY d.name, f.path, dep.line_number
|
|
134
|
-
`;
|
|
135
|
-
return this.db.query<any, []>(query).all();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
public getSymbolImpact(symbolQuery: string): GetSymbolImpactResponse {
|
|
139
|
-
const trimmed = symbolQuery.trim();
|
|
140
|
-
if (!trimmed) {
|
|
141
|
-
return {
|
|
142
|
-
target_symbol: symbolQuery,
|
|
143
|
-
impact_summary: {
|
|
144
|
-
total_dependents: 0,
|
|
145
|
-
risk_level: "low",
|
|
146
|
-
affected_files_count: 0,
|
|
147
|
-
},
|
|
148
|
-
inbound_callers: [],
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let targetContainer = trimmed;
|
|
153
|
-
let targetMethod = "";
|
|
154
|
-
if (trimmed.includes("::")) {
|
|
155
|
-
const parts = trimmed.split("::");
|
|
156
|
-
targetContainer = parts[0];
|
|
157
|
-
targetMethod = parts[1];
|
|
158
|
-
} else if (trimmed.includes("@")) {
|
|
159
|
-
const parts = trimmed.split("@");
|
|
160
|
-
targetContainer = parts[0];
|
|
161
|
-
targetMethod = parts[1];
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// 1. Inbound dependencies from dependencies table
|
|
165
|
-
const depSql = `
|
|
166
|
-
SELECT
|
|
167
|
-
f.path AS file,
|
|
168
|
-
d.line_number,
|
|
169
|
-
d.target_symbol_or_path,
|
|
170
|
-
dom.name AS domain_name
|
|
171
|
-
FROM dependencies d
|
|
172
|
-
JOIN files f ON d.source_file_id = f.id
|
|
173
|
-
JOIN domains dom ON f.domain_id = dom.id
|
|
174
|
-
WHERE d.target_symbol_or_path LIKE ?
|
|
175
|
-
OR d.target_symbol_or_path LIKE ?
|
|
176
|
-
ORDER BY f.path ASC, d.line_number ASC
|
|
177
|
-
`;
|
|
178
|
-
const depRows = this.db
|
|
179
|
-
.query<any, [string, string]>(depSql)
|
|
180
|
-
.all(`%${targetContainer}%`, `%${trimmed}%`);
|
|
181
|
-
|
|
182
|
-
const callersMap = new Map<string, InboundCaller>();
|
|
183
|
-
|
|
184
|
-
for (const row of depRows) {
|
|
185
|
-
// Find enclosing symbol in caller file if any
|
|
186
|
-
const enclosing = this.db
|
|
187
|
-
.query<{ name: string; kind: string }, [string, number]>(
|
|
188
|
-
`SELECT s.name, s.kind
|
|
189
|
-
FROM symbols s
|
|
190
|
-
JOIN files f ON s.file_id = f.id
|
|
191
|
-
WHERE f.path = ? AND ? BETWEEN s.line_start AND s.line_end
|
|
192
|
-
ORDER BY (s.line_end - s.line_start) ASC LIMIT 1`
|
|
193
|
-
)
|
|
194
|
-
.get(row.file, row.line_number);
|
|
195
|
-
|
|
196
|
-
const callerName = enclosing ? enclosing.name : `${row.file}:${row.line_number}`;
|
|
197
|
-
const key = `${row.file}:${row.line_number}:${callerName}`;
|
|
198
|
-
|
|
199
|
-
if (!callersMap.has(key)) {
|
|
200
|
-
callersMap.set(key, {
|
|
201
|
-
file: row.file,
|
|
202
|
-
caller: callerName,
|
|
203
|
-
line: row.line_number,
|
|
204
|
-
kind: enclosing?.kind || "import",
|
|
205
|
-
domain: row.domain_name,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// 2. Scan file content of callers or related files for exact method calls if targetMethod specified
|
|
211
|
-
if (targetMethod) {
|
|
212
|
-
// Look for files importing targetContainer and inspect if they call targetMethod
|
|
213
|
-
const uniqueFiles = Array.from(new Set(depRows.map((r) => r.file)));
|
|
214
|
-
for (const filePath of uniqueFiles) {
|
|
215
|
-
try {
|
|
216
|
-
if (fs.existsSync(filePath)) {
|
|
217
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
218
|
-
const lines = content.split(/\r?\n/);
|
|
219
|
-
for (let i = 0; i < lines.length; i++) {
|
|
220
|
-
const line = lines[i];
|
|
221
|
-
if (
|
|
222
|
-
line.includes(`->${targetMethod}(`) ||
|
|
223
|
-
line.includes(`::${targetMethod}(`)
|
|
224
|
-
) {
|
|
225
|
-
const lineNum = i + 1;
|
|
226
|
-
const enclosing = this.db
|
|
227
|
-
.query<{ name: string; kind: string }, [string, number]>(
|
|
228
|
-
`SELECT s.name, s.kind
|
|
229
|
-
FROM symbols s
|
|
230
|
-
JOIN files f ON s.file_id = f.id
|
|
231
|
-
WHERE f.path = ? AND ? BETWEEN s.line_start AND s.line_end
|
|
232
|
-
ORDER BY (s.line_end - s.line_start) ASC LIMIT 1`
|
|
233
|
-
)
|
|
234
|
-
.get(filePath, lineNum);
|
|
235
|
-
|
|
236
|
-
const callerName = enclosing ? enclosing.name : `${filePath}:${lineNum}`;
|
|
237
|
-
const key = `${filePath}:${lineNum}:${callerName}`;
|
|
238
|
-
if (!callersMap.has(key)) {
|
|
239
|
-
callersMap.set(key, {
|
|
240
|
-
file: filePath,
|
|
241
|
-
caller: callerName,
|
|
242
|
-
line: lineNum,
|
|
243
|
-
kind: enclosing?.kind || "call",
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
} catch {
|
|
250
|
-
// Ignore read errors
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const inboundCallers = Array.from(callersMap.values());
|
|
256
|
-
const affectedFiles = new Set(inboundCallers.map((c) => c.file));
|
|
257
|
-
const distinctDomains = new Set(
|
|
258
|
-
inboundCallers.map((c) => c.domain).filter(Boolean)
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
let riskLevel: "low" | "medium" | "high" = "low";
|
|
262
|
-
if (affectedFiles.size > 5 || distinctDomains.size > 2) {
|
|
263
|
-
riskLevel = "high";
|
|
264
|
-
} else if (affectedFiles.size >= 2 || distinctDomains.size >= 2) {
|
|
265
|
-
riskLevel = "medium";
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
target_symbol: trimmed,
|
|
270
|
-
impact_summary: {
|
|
271
|
-
total_dependents: inboundCallers.length,
|
|
272
|
-
risk_level: riskLevel,
|
|
273
|
-
affected_files_count: affectedFiles.size,
|
|
274
|
-
},
|
|
275
|
-
inbound_callers: inboundCallers,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { Database } from "bun:sqlite";
|
|
3
|
+
import type {
|
|
4
|
+
ExtractedDependency,
|
|
5
|
+
GetSymbolImpactResponse,
|
|
6
|
+
InboundCaller,
|
|
7
|
+
VerticalSliceRecord,
|
|
8
|
+
} from "../../../types/index.ts";
|
|
9
|
+
|
|
10
|
+
export class DependencyRepository {
|
|
11
|
+
constructor(private db: Database) {}
|
|
12
|
+
|
|
13
|
+
public replaceFileDependencies(fileId: number, deps: ExtractedDependency[]): void {
|
|
14
|
+
this.db.query("DELETE FROM dependencies WHERE source_file_id = ?").run(fileId);
|
|
15
|
+
if (deps.length === 0) return;
|
|
16
|
+
|
|
17
|
+
const stmt = this.db.query(
|
|
18
|
+
`INSERT INTO dependencies (source_file_id, target_symbol_or_path, import_statement, line_number, is_external)
|
|
19
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
for (const dep of deps) {
|
|
23
|
+
stmt.run(fileId, dep.target, dep.statement, dep.line_number, dep.is_external ? 1 : 0);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public upsertVerticalSlice(
|
|
28
|
+
slice: Omit<VerticalSliceRecord, "id" | "created_at">
|
|
29
|
+
): void {
|
|
30
|
+
const existing = this.db
|
|
31
|
+
.query<VerticalSliceRecord, [string, string]>(
|
|
32
|
+
"SELECT id FROM vertical_slices WHERE http_method = ? AND route_uri = ?"
|
|
33
|
+
)
|
|
34
|
+
.get(slice.http_method, slice.route_uri);
|
|
35
|
+
|
|
36
|
+
if (existing) {
|
|
37
|
+
this.db
|
|
38
|
+
.query(
|
|
39
|
+
`UPDATE vertical_slices
|
|
40
|
+
SET domain_id = ?, feature_key = ?, route_name = ?, controller_class = ?,
|
|
41
|
+
action_name = ?, controller_file = ?, controller_line = ?,
|
|
42
|
+
architecture_style = ?, entry_kind = ?, execution_chain_json = ?
|
|
43
|
+
WHERE id = ?`
|
|
44
|
+
)
|
|
45
|
+
.run(
|
|
46
|
+
slice.domain_id,
|
|
47
|
+
slice.feature_key,
|
|
48
|
+
slice.route_name,
|
|
49
|
+
slice.controller_class,
|
|
50
|
+
slice.action_name,
|
|
51
|
+
slice.controller_file,
|
|
52
|
+
slice.controller_line,
|
|
53
|
+
slice.architecture_style ?? "clean",
|
|
54
|
+
slice.entry_kind ?? "http_route",
|
|
55
|
+
slice.execution_chain_json,
|
|
56
|
+
existing.id
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
this.db
|
|
60
|
+
.query(
|
|
61
|
+
`INSERT INTO vertical_slices (
|
|
62
|
+
domain_id, feature_key, http_method, route_uri, route_name,
|
|
63
|
+
controller_class, action_name, controller_file, controller_line,
|
|
64
|
+
architecture_style, entry_kind, execution_chain_json
|
|
65
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
66
|
+
)
|
|
67
|
+
.run(
|
|
68
|
+
slice.domain_id,
|
|
69
|
+
slice.feature_key,
|
|
70
|
+
slice.http_method,
|
|
71
|
+
slice.route_uri,
|
|
72
|
+
slice.route_name,
|
|
73
|
+
slice.controller_class,
|
|
74
|
+
slice.action_name,
|
|
75
|
+
slice.controller_file,
|
|
76
|
+
slice.controller_line,
|
|
77
|
+
slice.architecture_style ?? "clean",
|
|
78
|
+
slice.entry_kind ?? "http_route",
|
|
79
|
+
slice.execution_chain_json
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public findVerticalSlices(searchTerm: string): VerticalSliceRecord[] {
|
|
85
|
+
const term = searchTerm.trim();
|
|
86
|
+
const wild = `%${term}%`;
|
|
87
|
+
const query = `
|
|
88
|
+
SELECT * FROM vertical_slices
|
|
89
|
+
WHERE route_uri LIKE ?
|
|
90
|
+
OR route_name LIKE ?
|
|
91
|
+
OR controller_class LIKE ?
|
|
92
|
+
OR action_name LIKE ?
|
|
93
|
+
OR feature_key LIKE ?
|
|
94
|
+
OR execution_chain_json LIKE ?
|
|
95
|
+
ORDER BY id ASC
|
|
96
|
+
`;
|
|
97
|
+
return this.db.query<VerticalSliceRecord, [string, string, string, string, string, string]>(query).all(
|
|
98
|
+
wild, wild, wild, wild, wild, wild
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public getAllVerticalSlices(): VerticalSliceRecord[] {
|
|
103
|
+
return this.db.query<VerticalSliceRecord, []>("SELECT * FROM vertical_slices ORDER BY id ASC").all();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public clearVerticalSlices(domainId?: number): void {
|
|
107
|
+
if (domainId) {
|
|
108
|
+
this.db.query("DELETE FROM vertical_slices WHERE domain_id = ?").run(domainId);
|
|
109
|
+
} else {
|
|
110
|
+
this.db.query("DELETE FROM vertical_slices").run();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public getAllDependenciesWithDomains(): Array<{
|
|
115
|
+
source_file: string;
|
|
116
|
+
source_domain: string;
|
|
117
|
+
line_number: number;
|
|
118
|
+
target: string;
|
|
119
|
+
statement: string;
|
|
120
|
+
is_external: boolean;
|
|
121
|
+
}> {
|
|
122
|
+
const query = `
|
|
123
|
+
SELECT
|
|
124
|
+
f.path AS source_file,
|
|
125
|
+
d.name AS source_domain,
|
|
126
|
+
dep.line_number,
|
|
127
|
+
dep.target_symbol_or_path AS target,
|
|
128
|
+
dep.import_statement AS statement,
|
|
129
|
+
dep.is_external
|
|
130
|
+
FROM dependencies dep
|
|
131
|
+
JOIN files f ON dep.source_file_id = f.id
|
|
132
|
+
JOIN domains d ON f.domain_id = d.id
|
|
133
|
+
ORDER BY d.name, f.path, dep.line_number
|
|
134
|
+
`;
|
|
135
|
+
return this.db.query<any, []>(query).all();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public getSymbolImpact(symbolQuery: string): GetSymbolImpactResponse {
|
|
139
|
+
const trimmed = symbolQuery.trim();
|
|
140
|
+
if (!trimmed) {
|
|
141
|
+
return {
|
|
142
|
+
target_symbol: symbolQuery,
|
|
143
|
+
impact_summary: {
|
|
144
|
+
total_dependents: 0,
|
|
145
|
+
risk_level: "low",
|
|
146
|
+
affected_files_count: 0,
|
|
147
|
+
},
|
|
148
|
+
inbound_callers: [],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let targetContainer = trimmed;
|
|
153
|
+
let targetMethod = "";
|
|
154
|
+
if (trimmed.includes("::")) {
|
|
155
|
+
const parts = trimmed.split("::");
|
|
156
|
+
targetContainer = parts[0];
|
|
157
|
+
targetMethod = parts[1];
|
|
158
|
+
} else if (trimmed.includes("@")) {
|
|
159
|
+
const parts = trimmed.split("@");
|
|
160
|
+
targetContainer = parts[0];
|
|
161
|
+
targetMethod = parts[1];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 1. Inbound dependencies from dependencies table
|
|
165
|
+
const depSql = `
|
|
166
|
+
SELECT
|
|
167
|
+
f.path AS file,
|
|
168
|
+
d.line_number,
|
|
169
|
+
d.target_symbol_or_path,
|
|
170
|
+
dom.name AS domain_name
|
|
171
|
+
FROM dependencies d
|
|
172
|
+
JOIN files f ON d.source_file_id = f.id
|
|
173
|
+
JOIN domains dom ON f.domain_id = dom.id
|
|
174
|
+
WHERE d.target_symbol_or_path LIKE ?
|
|
175
|
+
OR d.target_symbol_or_path LIKE ?
|
|
176
|
+
ORDER BY f.path ASC, d.line_number ASC
|
|
177
|
+
`;
|
|
178
|
+
const depRows = this.db
|
|
179
|
+
.query<any, [string, string]>(depSql)
|
|
180
|
+
.all(`%${targetContainer}%`, `%${trimmed}%`);
|
|
181
|
+
|
|
182
|
+
const callersMap = new Map<string, InboundCaller>();
|
|
183
|
+
|
|
184
|
+
for (const row of depRows) {
|
|
185
|
+
// Find enclosing symbol in caller file if any
|
|
186
|
+
const enclosing = this.db
|
|
187
|
+
.query<{ name: string; kind: string }, [string, number]>(
|
|
188
|
+
`SELECT s.name, s.kind
|
|
189
|
+
FROM symbols s
|
|
190
|
+
JOIN files f ON s.file_id = f.id
|
|
191
|
+
WHERE f.path = ? AND ? BETWEEN s.line_start AND s.line_end
|
|
192
|
+
ORDER BY (s.line_end - s.line_start) ASC LIMIT 1`
|
|
193
|
+
)
|
|
194
|
+
.get(row.file, row.line_number);
|
|
195
|
+
|
|
196
|
+
const callerName = enclosing ? enclosing.name : `${row.file}:${row.line_number}`;
|
|
197
|
+
const key = `${row.file}:${row.line_number}:${callerName}`;
|
|
198
|
+
|
|
199
|
+
if (!callersMap.has(key)) {
|
|
200
|
+
callersMap.set(key, {
|
|
201
|
+
file: row.file,
|
|
202
|
+
caller: callerName,
|
|
203
|
+
line: row.line_number,
|
|
204
|
+
kind: enclosing?.kind || "import",
|
|
205
|
+
domain: row.domain_name,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 2. Scan file content of callers or related files for exact method calls if targetMethod specified
|
|
211
|
+
if (targetMethod) {
|
|
212
|
+
// Look for files importing targetContainer and inspect if they call targetMethod
|
|
213
|
+
const uniqueFiles = Array.from(new Set(depRows.map((r) => r.file)));
|
|
214
|
+
for (const filePath of uniqueFiles) {
|
|
215
|
+
try {
|
|
216
|
+
if (fs.existsSync(filePath)) {
|
|
217
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
218
|
+
const lines = content.split(/\r?\n/);
|
|
219
|
+
for (let i = 0; i < lines.length; i++) {
|
|
220
|
+
const line = lines[i];
|
|
221
|
+
if (
|
|
222
|
+
line.includes(`->${targetMethod}(`) ||
|
|
223
|
+
line.includes(`::${targetMethod}(`)
|
|
224
|
+
) {
|
|
225
|
+
const lineNum = i + 1;
|
|
226
|
+
const enclosing = this.db
|
|
227
|
+
.query<{ name: string; kind: string }, [string, number]>(
|
|
228
|
+
`SELECT s.name, s.kind
|
|
229
|
+
FROM symbols s
|
|
230
|
+
JOIN files f ON s.file_id = f.id
|
|
231
|
+
WHERE f.path = ? AND ? BETWEEN s.line_start AND s.line_end
|
|
232
|
+
ORDER BY (s.line_end - s.line_start) ASC LIMIT 1`
|
|
233
|
+
)
|
|
234
|
+
.get(filePath, lineNum);
|
|
235
|
+
|
|
236
|
+
const callerName = enclosing ? enclosing.name : `${filePath}:${lineNum}`;
|
|
237
|
+
const key = `${filePath}:${lineNum}:${callerName}`;
|
|
238
|
+
if (!callersMap.has(key)) {
|
|
239
|
+
callersMap.set(key, {
|
|
240
|
+
file: filePath,
|
|
241
|
+
caller: callerName,
|
|
242
|
+
line: lineNum,
|
|
243
|
+
kind: enclosing?.kind || "call",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// Ignore read errors
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const inboundCallers = Array.from(callersMap.values());
|
|
256
|
+
const affectedFiles = new Set(inboundCallers.map((c) => c.file));
|
|
257
|
+
const distinctDomains = new Set(
|
|
258
|
+
inboundCallers.map((c) => c.domain).filter(Boolean)
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
let riskLevel: "low" | "medium" | "high" = "low";
|
|
262
|
+
if (affectedFiles.size > 5 || distinctDomains.size > 2) {
|
|
263
|
+
riskLevel = "high";
|
|
264
|
+
} else if (affectedFiles.size >= 2 || distinctDomains.size >= 2) {
|
|
265
|
+
riskLevel = "medium";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
target_symbol: trimmed,
|
|
270
|
+
impact_summary: {
|
|
271
|
+
total_dependents: inboundCallers.length,
|
|
272
|
+
risk_level: riskLevel,
|
|
273
|
+
affected_files_count: affectedFiles.size,
|
|
274
|
+
},
|
|
275
|
+
inbound_callers: inboundCallers,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
}
|