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,349 +1,552 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
slice.
|
|
53
|
-
slice.
|
|
54
|
-
slice.
|
|
55
|
-
slice.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
slice.
|
|
75
|
-
slice.
|
|
76
|
-
slice.
|
|
77
|
-
slice.
|
|
78
|
-
slice.
|
|
79
|
-
slice.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return this.db.query<VerticalSliceRecord, [
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { dirname, join, posix } from "node:path";
|
|
3
|
+
import type { Database } from "bun:sqlite";
|
|
4
|
+
import type {
|
|
5
|
+
ExtractedDependency,
|
|
6
|
+
FileConnectionsSummary,
|
|
7
|
+
GetSymbolImpactResponse,
|
|
8
|
+
InboundCaller,
|
|
9
|
+
VerticalSliceRecord,
|
|
10
|
+
} from "../../../types/index.ts";
|
|
11
|
+
|
|
12
|
+
function escapeRegExp(str: string): string {
|
|
13
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class DependencyRepository {
|
|
17
|
+
constructor(private db: Database) {}
|
|
18
|
+
|
|
19
|
+
public replaceFileDependencies(fileId: number, deps: ExtractedDependency[]): void {
|
|
20
|
+
this.db.query("DELETE FROM dependencies WHERE source_file_id = ?").run(fileId);
|
|
21
|
+
if (deps.length === 0) return;
|
|
22
|
+
|
|
23
|
+
const stmt = this.db.query(
|
|
24
|
+
`INSERT INTO dependencies (source_file_id, target_symbol_or_path, import_statement, line_number, is_external)
|
|
25
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
for (const dep of deps) {
|
|
29
|
+
stmt.run(fileId, dep.target, dep.statement, dep.line_number, dep.is_external ? 1 : 0);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public upsertVerticalSlice(
|
|
34
|
+
slice: Omit<VerticalSliceRecord, "id" | "created_at">
|
|
35
|
+
): void {
|
|
36
|
+
const existing = this.db
|
|
37
|
+
.query<VerticalSliceRecord, [string, string]>(
|
|
38
|
+
"SELECT id FROM vertical_slices WHERE http_method = ? AND route_uri = ?"
|
|
39
|
+
)
|
|
40
|
+
.get(slice.http_method, slice.route_uri);
|
|
41
|
+
|
|
42
|
+
if (existing) {
|
|
43
|
+
this.db
|
|
44
|
+
.query(
|
|
45
|
+
`UPDATE vertical_slices
|
|
46
|
+
SET domain_id = ?, feature_key = ?, route_name = ?, controller_class = ?,
|
|
47
|
+
action_name = ?, controller_file = ?, controller_line = ?,
|
|
48
|
+
architecture_style = ?, entry_kind = ?, execution_chain_json = ?
|
|
49
|
+
WHERE id = ?`
|
|
50
|
+
)
|
|
51
|
+
.run(
|
|
52
|
+
slice.domain_id,
|
|
53
|
+
slice.feature_key,
|
|
54
|
+
slice.route_name,
|
|
55
|
+
slice.controller_class,
|
|
56
|
+
slice.action_name,
|
|
57
|
+
slice.controller_file,
|
|
58
|
+
slice.controller_line,
|
|
59
|
+
slice.architecture_style ?? "clean",
|
|
60
|
+
slice.entry_kind ?? "http_route",
|
|
61
|
+
slice.execution_chain_json,
|
|
62
|
+
existing.id
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
this.db
|
|
66
|
+
.query(
|
|
67
|
+
`INSERT INTO vertical_slices (
|
|
68
|
+
domain_id, feature_key, http_method, route_uri, route_name,
|
|
69
|
+
controller_class, action_name, controller_file, controller_line,
|
|
70
|
+
architecture_style, entry_kind, execution_chain_json
|
|
71
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
72
|
+
)
|
|
73
|
+
.run(
|
|
74
|
+
slice.domain_id,
|
|
75
|
+
slice.feature_key,
|
|
76
|
+
slice.http_method,
|
|
77
|
+
slice.route_uri,
|
|
78
|
+
slice.route_name,
|
|
79
|
+
slice.controller_class,
|
|
80
|
+
slice.action_name,
|
|
81
|
+
slice.controller_file,
|
|
82
|
+
slice.controller_line,
|
|
83
|
+
slice.architecture_style ?? "clean",
|
|
84
|
+
slice.entry_kind ?? "http_route",
|
|
85
|
+
slice.execution_chain_json
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public findVerticalSlices(searchTerm: string): VerticalSliceRecord[] {
|
|
91
|
+
const term = searchTerm.trim();
|
|
92
|
+
const wild = `%${term}%`;
|
|
93
|
+
const query = `
|
|
94
|
+
SELECT * FROM vertical_slices
|
|
95
|
+
WHERE route_uri LIKE ?
|
|
96
|
+
OR route_name LIKE ?
|
|
97
|
+
OR controller_class LIKE ?
|
|
98
|
+
OR action_name LIKE ?
|
|
99
|
+
OR feature_key LIKE ?
|
|
100
|
+
OR execution_chain_json LIKE ?
|
|
101
|
+
ORDER BY id ASC
|
|
102
|
+
`;
|
|
103
|
+
return this.db.query<VerticalSliceRecord, [string, string, string, string, string, string]>(query).all(
|
|
104
|
+
wild, wild, wild, wild, wild, wild
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public getAllVerticalSlices(): VerticalSliceRecord[] {
|
|
109
|
+
return this.db.query<VerticalSliceRecord, []>("SELECT * FROM vertical_slices ORDER BY id ASC").all();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public countVerticalSlices(): number {
|
|
113
|
+
const row = this.db
|
|
114
|
+
.query<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM vertical_slices")
|
|
115
|
+
.get();
|
|
116
|
+
return row?.cnt ?? 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public clearVerticalSlices(domainId?: number): void {
|
|
120
|
+
if (domainId) {
|
|
121
|
+
this.db.query("DELETE FROM vertical_slices WHERE domain_id = ?").run(domainId);
|
|
122
|
+
} else {
|
|
123
|
+
this.db.query("DELETE FROM vertical_slices").run();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public getAllDependenciesWithDomains(): Array<{
|
|
128
|
+
source_file: string;
|
|
129
|
+
source_domain: string;
|
|
130
|
+
line_number: number;
|
|
131
|
+
target: string;
|
|
132
|
+
statement: string;
|
|
133
|
+
is_external: boolean;
|
|
134
|
+
}> {
|
|
135
|
+
const query = `
|
|
136
|
+
SELECT
|
|
137
|
+
f.path AS source_file,
|
|
138
|
+
d.name AS source_domain,
|
|
139
|
+
dep.line_number,
|
|
140
|
+
dep.target_symbol_or_path AS target,
|
|
141
|
+
dep.import_statement AS statement,
|
|
142
|
+
dep.is_external
|
|
143
|
+
FROM dependencies dep
|
|
144
|
+
JOIN files f ON dep.source_file_id = f.id
|
|
145
|
+
JOIN domains d ON f.domain_id = d.id
|
|
146
|
+
ORDER BY d.name, f.path, dep.line_number
|
|
147
|
+
`;
|
|
148
|
+
return this.db.query<any, []>(query).all();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public getDomainFileConnections(domainId?: number): Map<string, FileConnectionsSummary> {
|
|
152
|
+
const allFiles = this.db
|
|
153
|
+
.query<{ id: number; domain_id: number; path: string }, []>(
|
|
154
|
+
"SELECT id, domain_id, path FROM files ORDER BY path ASC"
|
|
155
|
+
)
|
|
156
|
+
.all();
|
|
157
|
+
|
|
158
|
+
const fileById = new Map<number, { id: number; domain_id: number; path: string }>();
|
|
159
|
+
const fileByNormPath = new Map<string, string>();
|
|
160
|
+
const fileByNoExtPath = new Map<string, string>();
|
|
161
|
+
|
|
162
|
+
for (const f of allFiles) {
|
|
163
|
+
const norm = f.path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
164
|
+
fileById.set(f.id, { ...f, path: norm });
|
|
165
|
+
fileByNormPath.set(norm.toLowerCase(), f.path);
|
|
166
|
+
const noExt = norm.replace(/\.[^/.]+$/, "").toLowerCase();
|
|
167
|
+
fileByNoExtPath.set(noExt, f.path);
|
|
168
|
+
if (noExt.endsWith("/index")) {
|
|
169
|
+
fileByNoExtPath.set(noExt.slice(0, -"/index".length), f.path);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const resolveInternalTarget = (sourcePath: string, rawTarget: string): string | null => {
|
|
174
|
+
const cleaned = rawTarget.trim().replace(/^['"]|['"]$/g, "").replace(/\\/g, "/");
|
|
175
|
+
if (!cleaned) return null;
|
|
176
|
+
|
|
177
|
+
if (cleaned.startsWith(".")) {
|
|
178
|
+
const srcDir = posix.dirname(sourcePath.replace(/\\/g, "/"));
|
|
179
|
+
const joined = posix.normalize(posix.join(srcDir, cleaned)).replace(/^\.\//, "");
|
|
180
|
+
const lowerJoined = joined.toLowerCase();
|
|
181
|
+
const exact = fileByNormPath.get(lowerJoined) ?? fileByNoExtPath.get(lowerJoined.replace(/\.[^/.]+$/, ""));
|
|
182
|
+
if (exact) return exact;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const aliasClean = cleaned.replace(/^@\//, "src/").replace(/^\.\//, "");
|
|
186
|
+
const lowerAlias = aliasClean.toLowerCase();
|
|
187
|
+
const byAlias = fileByNormPath.get(lowerAlias) ?? fileByNoExtPath.get(lowerAlias.replace(/\.[^/.]+$/, ""));
|
|
188
|
+
if (byAlias) return byAlias;
|
|
189
|
+
|
|
190
|
+
for (const [noExt, origPath] of fileByNoExtPath.entries()) {
|
|
191
|
+
if (noExt.endsWith(`/${lowerAlias}`)) {
|
|
192
|
+
return origPath;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const importsFromMap = new Map<string, Set<string>>();
|
|
199
|
+
const importedByMap = new Map<string, Set<string>>();
|
|
200
|
+
for (const f of allFiles) {
|
|
201
|
+
importsFromMap.set(f.path, new Set());
|
|
202
|
+
importedByMap.set(f.path, new Set());
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const allInternalDeps = this.db
|
|
206
|
+
.query<{ source_file_id: number; target_symbol_or_path: string }, []>(
|
|
207
|
+
"SELECT source_file_id, target_symbol_or_path FROM dependencies WHERE is_external = 0"
|
|
208
|
+
)
|
|
209
|
+
.all();
|
|
210
|
+
|
|
211
|
+
for (const dep of allInternalDeps) {
|
|
212
|
+
const srcFile = fileById.get(dep.source_file_id);
|
|
213
|
+
if (!srcFile) continue;
|
|
214
|
+
const resolvedTarget = resolveInternalTarget(srcFile.path, dep.target_symbol_or_path);
|
|
215
|
+
if (resolvedTarget && resolvedTarget !== srcFile.path) {
|
|
216
|
+
importsFromMap.get(srcFile.path)?.add(resolvedTarget);
|
|
217
|
+
if (!importedByMap.has(resolvedTarget)) {
|
|
218
|
+
importedByMap.set(resolvedTarget, new Set());
|
|
219
|
+
}
|
|
220
|
+
importedByMap.get(resolvedTarget)?.add(srcFile.path);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result = new Map<string, FileConnectionsSummary>();
|
|
225
|
+
for (const f of allFiles) {
|
|
226
|
+
if (domainId !== undefined && f.domain_id !== domainId) continue;
|
|
227
|
+
const importsFrom = Array.from(importsFromMap.get(f.path) ?? []).sort();
|
|
228
|
+
const importedBy = Array.from(importedByMap.get(f.path) ?? []).sort();
|
|
229
|
+
result.set(f.path, {
|
|
230
|
+
imports_from: importsFrom,
|
|
231
|
+
imported_by_count: importedBy.length,
|
|
232
|
+
imported_by: importedBy,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public getFileConnections(filePath: string): FileConnectionsSummary {
|
|
240
|
+
const clean = filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
241
|
+
const allConns = this.getDomainFileConnections();
|
|
242
|
+
return (
|
|
243
|
+
allConns.get(clean) ??
|
|
244
|
+
allConns.get(filePath) ?? {
|
|
245
|
+
imports_from: [],
|
|
246
|
+
imported_by_count: 0,
|
|
247
|
+
imported_by: [],
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public getSymbolImpact(symbolQuery: string): GetSymbolImpactResponse {
|
|
253
|
+
const trimmed = symbolQuery.trim();
|
|
254
|
+
if (!trimmed) {
|
|
255
|
+
return {
|
|
256
|
+
target_symbol: symbolQuery,
|
|
257
|
+
found_in_index: false,
|
|
258
|
+
warning: "Empty symbol query provided.",
|
|
259
|
+
impact_summary: {
|
|
260
|
+
total_dependents: 0,
|
|
261
|
+
risk_level: "low",
|
|
262
|
+
affected_files_count: 0,
|
|
263
|
+
},
|
|
264
|
+
inbound_callers: [],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let targetContainer = trimmed;
|
|
269
|
+
let targetMethod = "";
|
|
270
|
+
if (trimmed.includes("::")) {
|
|
271
|
+
const parts = trimmed.split("::");
|
|
272
|
+
targetContainer = parts[0];
|
|
273
|
+
targetMethod = parts[1];
|
|
274
|
+
} else if (trimmed.includes("@")) {
|
|
275
|
+
const parts = trimmed.split("@");
|
|
276
|
+
targetContainer = parts[0];
|
|
277
|
+
targetMethod = parts[1];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 1. Look up target symbol in symbols joined with files to find its defining file_path & stem
|
|
281
|
+
const targetSymRows = this.db
|
|
282
|
+
.query<{ name: string; file_path: string }, [string, string, string, string]>(
|
|
283
|
+
`SELECT s.name, f.path AS file_path
|
|
284
|
+
FROM symbols s
|
|
285
|
+
JOIN files f ON s.file_id = f.id
|
|
286
|
+
WHERE s.name = ? OR s.name = ? OR s.name LIKE ? OR s.name LIKE ?`
|
|
287
|
+
)
|
|
288
|
+
.all(trimmed, targetContainer, `${targetContainer}::%`, `%::${targetContainer}`);
|
|
289
|
+
|
|
290
|
+
const definingFiles = new Set<string>(
|
|
291
|
+
targetSymRows.map((r) => r.file_path.replace(/\\/g, "/"))
|
|
292
|
+
);
|
|
293
|
+
const definingFileStems = Array.from(
|
|
294
|
+
new Set(
|
|
295
|
+
Array.from(definingFiles)
|
|
296
|
+
.map((p) => p.split("/").pop()?.replace(/\.[^.]+$/, "") || "")
|
|
297
|
+
.filter((s) => s.length >= 2)
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// 2. Query dependencies matching targetContainer, trimmed, import_statement, or defining file stem
|
|
302
|
+
const whereClauses: string[] = [
|
|
303
|
+
"d.target_symbol_or_path LIKE ?",
|
|
304
|
+
"d.import_statement LIKE ?",
|
|
305
|
+
"d.target_symbol_or_path LIKE ?",
|
|
306
|
+
];
|
|
307
|
+
const queryParams: string[] = [
|
|
308
|
+
`%${targetContainer}%`,
|
|
309
|
+
`%${targetContainer}%`,
|
|
310
|
+
`%${trimmed}%`,
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
for (const stem of definingFileStems) {
|
|
314
|
+
whereClauses.push("d.target_symbol_or_path LIKE ?");
|
|
315
|
+
queryParams.push(`%${stem}%`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const depSql = `
|
|
319
|
+
SELECT
|
|
320
|
+
f.path AS file,
|
|
321
|
+
d.line_number,
|
|
322
|
+
d.target_symbol_or_path,
|
|
323
|
+
d.import_statement,
|
|
324
|
+
dom.name AS domain_name
|
|
325
|
+
FROM dependencies d
|
|
326
|
+
JOIN files f ON d.source_file_id = f.id
|
|
327
|
+
LEFT JOIN domains dom ON f.domain_id = dom.id
|
|
328
|
+
WHERE ${whereClauses.join(" OR ")}
|
|
329
|
+
ORDER BY f.path ASC, d.line_number ASC
|
|
330
|
+
`;
|
|
331
|
+
const rawDepRows = this.db.query<any, string[]>(depSql).all(...queryParams);
|
|
332
|
+
|
|
333
|
+
const containerWordRegex = new RegExp(`\\b${escapeRegExp(targetContainer)}\\b`);
|
|
334
|
+
const depRows = rawDepRows.filter((row) => {
|
|
335
|
+
const normCallerFile = String(row.file).replace(/\\/g, "/");
|
|
336
|
+
if (definingFiles.has(normCallerFile)) return false;
|
|
337
|
+
|
|
338
|
+
const stmt = String(row.import_statement || "");
|
|
339
|
+
const target = String(row.target_symbol_or_path || "");
|
|
340
|
+
|
|
341
|
+
if (containerWordRegex.test(stmt) || containerWordRegex.test(target)) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// If no named specifier braces are present, accept path match against defining file stem
|
|
346
|
+
if (!stmt.includes("{")) {
|
|
347
|
+
for (const stem of definingFileStems) {
|
|
348
|
+
const stemRegex = new RegExp(`(?:^|[\\\\/])${escapeRegExp(stem)}(?:\\.[^\\\\/]+)?$`);
|
|
349
|
+
if (stemRegex.test(target)) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return false;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// 3. Batch-load all symbols for candidate caller files in ONE query (zero N+1)
|
|
359
|
+
const uniqueCallerFiles = Array.from(new Set(depRows.map((r) => String(r.file))));
|
|
360
|
+
const callerSymbolsByFile = new Map<
|
|
361
|
+
string,
|
|
362
|
+
Array<{ name: string; kind: string; line_start: number; line_end: number }>
|
|
363
|
+
>();
|
|
364
|
+
|
|
365
|
+
if (uniqueCallerFiles.length > 0) {
|
|
366
|
+
const placeholders = uniqueCallerFiles.map(() => "?").join(",");
|
|
367
|
+
const allCallerSyms = this.db
|
|
368
|
+
.query<
|
|
369
|
+
{ name: string; kind: string; line_start: number; line_end: number; path: string },
|
|
370
|
+
string[]
|
|
371
|
+
>(
|
|
372
|
+
`SELECT s.name, s.kind, s.line_start, s.line_end, f.path
|
|
373
|
+
FROM symbols s
|
|
374
|
+
JOIN files f ON s.file_id = f.id
|
|
375
|
+
WHERE f.path IN (${placeholders})
|
|
376
|
+
ORDER BY (s.line_end - s.line_start) ASC`
|
|
377
|
+
)
|
|
378
|
+
.all(...uniqueCallerFiles);
|
|
379
|
+
|
|
380
|
+
for (const sym of allCallerSyms) {
|
|
381
|
+
const list = callerSymbolsByFile.get(sym.path) || [];
|
|
382
|
+
list.push(sym);
|
|
383
|
+
callerSymbolsByFile.set(sym.path, list);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const findEnclosingSymbol = (
|
|
388
|
+
filePath: string,
|
|
389
|
+
lineNum: number
|
|
390
|
+
): { name: string; kind: string } | undefined => {
|
|
391
|
+
const syms = callerSymbolsByFile.get(filePath);
|
|
392
|
+
if (!syms) return undefined;
|
|
393
|
+
return syms.find((s) => lineNum >= s.line_start && lineNum <= s.line_end);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const callersMap = new Map<string, InboundCaller>();
|
|
397
|
+
|
|
398
|
+
for (const row of depRows) {
|
|
399
|
+
const enclosing = findEnclosingSymbol(row.file, row.line_number);
|
|
400
|
+
const callerName = enclosing ? enclosing.name : `${row.file}:${row.line_number}`;
|
|
401
|
+
const key = `${row.file}:${row.line_number}:${callerName}`;
|
|
402
|
+
|
|
403
|
+
if (!callersMap.has(key)) {
|
|
404
|
+
callersMap.set(key, {
|
|
405
|
+
file: row.file,
|
|
406
|
+
caller: callerName,
|
|
407
|
+
line: row.line_number,
|
|
408
|
+
kind: enclosing?.kind || "import",
|
|
409
|
+
domain: row.domain_name ?? undefined,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 4. Scan file content of callers for exact method calls if targetMethod specified
|
|
415
|
+
if (targetMethod) {
|
|
416
|
+
for (const filePath of uniqueCallerFiles) {
|
|
417
|
+
try {
|
|
418
|
+
if (fs.existsSync(filePath)) {
|
|
419
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
420
|
+
const lines = content.split(/\r?\n/);
|
|
421
|
+
for (let i = 0; i < lines.length; i++) {
|
|
422
|
+
const line = lines[i];
|
|
423
|
+
if (
|
|
424
|
+
line.includes(`->${targetMethod}(`) ||
|
|
425
|
+
line.includes(`::${targetMethod}(`) ||
|
|
426
|
+
line.includes(`.${targetMethod}(`)
|
|
427
|
+
) {
|
|
428
|
+
const lineNum = i + 1;
|
|
429
|
+
const enclosing = findEnclosingSymbol(filePath, lineNum);
|
|
430
|
+
const callerName = enclosing ? enclosing.name : `${filePath}:${lineNum}`;
|
|
431
|
+
const key = `${filePath}:${lineNum}:${callerName}`;
|
|
432
|
+
if (!callersMap.has(key)) {
|
|
433
|
+
callersMap.set(key, {
|
|
434
|
+
file: filePath,
|
|
435
|
+
caller: callerName,
|
|
436
|
+
line: lineNum,
|
|
437
|
+
kind: enclosing?.kind || "call",
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} catch {
|
|
444
|
+
// Ignore read errors
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const inboundCallers = Array.from(callersMap.values());
|
|
450
|
+
const affectedFiles = new Set(inboundCallers.map((c) => c.file));
|
|
451
|
+
const distinctDomains = new Set(
|
|
452
|
+
inboundCallers.map((c) => c.domain).filter(Boolean)
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
let riskLevel: "low" | "medium" | "high" = "low";
|
|
456
|
+
if (affectedFiles.size > 5 || distinctDomains.size > 2) {
|
|
457
|
+
riskLevel = "high";
|
|
458
|
+
} else if (affectedFiles.size >= 2 || distinctDomains.size >= 2) {
|
|
459
|
+
riskLevel = "medium";
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 5. Determine if symbol exists in index
|
|
463
|
+
const foundInIndex = targetSymRows.length > 0 || depRows.length > 0;
|
|
464
|
+
const warning = foundInIndex
|
|
465
|
+
? undefined
|
|
466
|
+
: `Symbol '${trimmed}' was not found in Septum index (neither in declared symbols nor tracked dependencies). Risk assessment defaults to low (0 callers) due to unindexed status.`;
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
target_symbol: trimmed,
|
|
470
|
+
found_in_index: foundInIndex,
|
|
471
|
+
warning,
|
|
472
|
+
impact_summary: {
|
|
473
|
+
total_dependents: inboundCallers.length,
|
|
474
|
+
risk_level: riskLevel,
|
|
475
|
+
affected_files_count: affectedFiles.size,
|
|
476
|
+
},
|
|
477
|
+
inbound_callers: inboundCallers,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
public getFileInboundImpact(filePath: string): {
|
|
482
|
+
direct_dependents_count: number;
|
|
483
|
+
top_consumers: string[];
|
|
484
|
+
symbols_count: number;
|
|
485
|
+
risk_level: "low" | "medium" | "high";
|
|
486
|
+
} {
|
|
487
|
+
const cleanPath = filePath.replace(/\\/g, "/");
|
|
488
|
+
const baseName = cleanPath.split("/").pop()?.replace(/\.[^.]+$/, "") || "";
|
|
489
|
+
|
|
490
|
+
const symbols = this.db
|
|
491
|
+
.query<{ name: string }, [string, string]>(
|
|
492
|
+
`SELECT s.name
|
|
493
|
+
FROM symbols s
|
|
494
|
+
JOIN files f ON s.file_id = f.id
|
|
495
|
+
WHERE f.path = ? OR f.path LIKE ?`
|
|
496
|
+
)
|
|
497
|
+
.all(cleanPath, `%${cleanPath}`);
|
|
498
|
+
|
|
499
|
+
const matchedFiles = new Set<string>();
|
|
500
|
+
const orConditions: string[] = [];
|
|
501
|
+
const params: string[] = [];
|
|
502
|
+
|
|
503
|
+
if (baseName.length >= 3) {
|
|
504
|
+
orConditions.push("d.target_symbol_or_path LIKE ?");
|
|
505
|
+
params.push(`%${cleanPath}%`);
|
|
506
|
+
orConditions.push("d.target_symbol_or_path LIKE ?");
|
|
507
|
+
params.push(`%${baseName}%`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for (const sym of symbols.slice(0, 8)) {
|
|
511
|
+
const symName = sym.name.split("::").pop() || sym.name;
|
|
512
|
+
if (symName.length >= 4) {
|
|
513
|
+
orConditions.push("d.target_symbol_or_path LIKE ?");
|
|
514
|
+
params.push(`%${symName}%`);
|
|
515
|
+
orConditions.push("d.import_statement LIKE ?");
|
|
516
|
+
params.push(`%${symName}%`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (orConditions.length > 0) {
|
|
521
|
+
params.push(`%${cleanPath}`);
|
|
522
|
+
const depRows = this.db
|
|
523
|
+
.query<{ file: string }, string[]>(
|
|
524
|
+
`SELECT DISTINCT f.path AS file
|
|
525
|
+
FROM dependencies d
|
|
526
|
+
JOIN files f ON d.source_file_id = f.id
|
|
527
|
+
WHERE (${orConditions.join(" OR ")})
|
|
528
|
+
AND f.path NOT LIKE ?`
|
|
529
|
+
)
|
|
530
|
+
.all(...params);
|
|
531
|
+
|
|
532
|
+
for (const row of depRows) {
|
|
533
|
+
matchedFiles.add(row.file);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const consumerList = Array.from(matchedFiles);
|
|
538
|
+
let riskLevel: "low" | "medium" | "high" = "low";
|
|
539
|
+
if (consumerList.length >= 5) {
|
|
540
|
+
riskLevel = "high";
|
|
541
|
+
} else if (consumerList.length >= 2) {
|
|
542
|
+
riskLevel = "medium";
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
direct_dependents_count: consumerList.length,
|
|
547
|
+
top_consumers: consumerList.slice(0, 5),
|
|
548
|
+
symbols_count: symbols.length,
|
|
549
|
+
risk_level: riskLevel,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
}
|