septum 0.1.0
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 -0
- package/README.md +100 -0
- package/bin/septum.ts +4 -0
- package/package.json +62 -0
- package/src/cli/commands/check.ts +161 -0
- package/src/cli/commands/hook.ts +188 -0
- package/src/cli/commands/ingest.ts +29 -0
- package/src/cli/commands/init.ts +211 -0
- package/src/cli/commands/locate.ts +61 -0
- package/src/cli/commands/query.ts +64 -0
- package/src/cli/commands/serve.ts +5 -0
- package/src/cli/commands/slice.ts +90 -0
- package/src/cli/commands/sync.ts +31 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/templates/hooks.ts +75 -0
- package/src/cli/templates/rules.ts +38 -0
- package/src/cli/templates/skill.ts +66 -0
- package/src/core/boundary/evaluator.ts +210 -0
- package/src/core/config/loader.ts +84 -0
- package/src/core/config/schema.ts +39 -0
- package/src/core/database/client.ts +132 -0
- package/src/core/database/repositories/dependency.repository.ts +279 -0
- package/src/core/database/repositories/domain.repository.ts +198 -0
- package/src/core/database/repositories/file.repository.ts +99 -0
- package/src/core/database/repositories/meta.repository.ts +33 -0
- package/src/core/database/repositories/symbol.repository.ts +424 -0
- package/src/core/database/repository.ts +242 -0
- package/src/core/database/schema.sql +92 -0
- package/src/core/discovery/topology-detector.ts +418 -0
- package/src/core/ingestion/hasher.ts +5 -0
- package/src/core/ingestion/pipeline.ts +438 -0
- package/src/core/parser/boundary-tracker.ts +128 -0
- package/src/core/parser/extractors/base.ts +60 -0
- package/src/core/parser/extractors/frontend.ts +194 -0
- package/src/core/parser/extractors/go.ts +176 -0
- package/src/core/parser/extractors/laravel-semantic.ts +426 -0
- package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -0
- package/src/core/parser/extractors/nestjs-semantic.ts +119 -0
- package/src/core/parser/extractors/php.ts +149 -0
- package/src/core/parser/extractors/python.ts +226 -0
- package/src/core/parser/extractors/registry.ts +49 -0
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -0
- package/src/core/parser/extractors/typescript.ts +177 -0
- package/src/core/parser/tree-sitter.ts +39 -0
- package/src/core/resolver/call-graph-tracer.ts +668 -0
- package/src/core/resolver/module-resolver.ts +323 -0
- package/src/core/resolver/symbol-locator.ts +405 -0
- package/src/core/resolver/vertical-slice-tracer.ts +236 -0
- package/src/core/session/session-manager.ts +56 -0
- package/src/core/telemetry/telemetry.ts +86 -0
- package/src/index.ts +18 -0
- package/src/mcp/schemas.ts +68 -0
- package/src/mcp/server.ts +602 -0
- package/src/mcp/tools/check-boundary.ts +102 -0
- package/src/mcp/tools/get-domain-catalog.ts +91 -0
- package/src/mcp/tools/get-feature-context.ts +116 -0
- package/src/mcp/tools/get-symbol-hotspots.ts +49 -0
- package/src/mcp/tools/get-symbol-impact.ts +26 -0
- package/src/mcp/tools/get-symbol.ts +63 -0
- package/src/mcp/tools/locate-symbol.ts +74 -0
- package/src/mcp/tools/register-domain.ts +64 -0
- package/src/mcp/tools/trace-vertical-slice.ts +102 -0
- package/src/types/index.ts +296 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { Database } from "bun:sqlite";
|
|
3
|
+
import type {
|
|
4
|
+
DomainRecord,
|
|
5
|
+
ExtractedSymbol,
|
|
6
|
+
FileRecord,
|
|
7
|
+
GetSymbolDetailsResponse,
|
|
8
|
+
SymbolKind,
|
|
9
|
+
SymbolRecord,
|
|
10
|
+
} from "../../../types/index.ts";
|
|
11
|
+
|
|
12
|
+
export class SymbolRepository {
|
|
13
|
+
constructor(private db: Database) {}
|
|
14
|
+
|
|
15
|
+
public replaceFileSymbols(fileId: number, symbols: ExtractedSymbol[]): void {
|
|
16
|
+
this.db.query("DELETE FROM symbols WHERE file_id = ?").run(fileId);
|
|
17
|
+
if (symbols.length === 0) return;
|
|
18
|
+
|
|
19
|
+
const stmt = this.db.query(
|
|
20
|
+
`INSERT INTO symbols (file_id, name, kind, signature, visibility, line_start, line_end, line_count)
|
|
21
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
for (const sym of symbols) {
|
|
25
|
+
const lineCount = sym.line_count ?? Math.max(1, sym.line_end - sym.line_start + 1);
|
|
26
|
+
stmt.run(
|
|
27
|
+
fileId,
|
|
28
|
+
sym.name,
|
|
29
|
+
sym.kind,
|
|
30
|
+
sym.signature,
|
|
31
|
+
sym.visibility,
|
|
32
|
+
sym.line_start,
|
|
33
|
+
sym.line_end,
|
|
34
|
+
lineCount
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public getSymbolsByFileId(fileId: number): SymbolRecord[] {
|
|
40
|
+
return this.db
|
|
41
|
+
.query<SymbolRecord, [number]>(
|
|
42
|
+
"SELECT * FROM symbols WHERE file_id = ? ORDER BY line_start ASC"
|
|
43
|
+
)
|
|
44
|
+
.all(fileId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public findSymbolsByName(name: string): Array<SymbolRecord & { file_path: string }> {
|
|
48
|
+
const query = `
|
|
49
|
+
SELECT s.*, f.path AS file_path
|
|
50
|
+
FROM symbols s
|
|
51
|
+
JOIN files f ON s.file_id = f.id
|
|
52
|
+
WHERE s.name = ? OR s.name LIKE ?
|
|
53
|
+
ORDER BY s.line_start ASC
|
|
54
|
+
`;
|
|
55
|
+
return this.db
|
|
56
|
+
.query<SymbolRecord & { file_path: string }, [string, string]>(query)
|
|
57
|
+
.all(name, `%::${name}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public findContainers(
|
|
61
|
+
containerName: string
|
|
62
|
+
): Array<{ file: FileRecord; symbol?: SymbolRecord }> {
|
|
63
|
+
const symbolQuery = `
|
|
64
|
+
SELECT s.*, f.path AS file_path, f.id AS f_id, f.domain_id AS f_domain_id,
|
|
65
|
+
f.archetype AS f_archetype, f.content_hash AS f_content_hash,
|
|
66
|
+
f.line_count AS f_line_count, f.mtime_ms AS f_mtime_ms,
|
|
67
|
+
f.size_bytes AS f_size_bytes, f.last_scanned_at AS f_last_scanned_at
|
|
68
|
+
FROM symbols s
|
|
69
|
+
JOIN files f ON s.file_id = f.id
|
|
70
|
+
WHERE (s.name = ? OR s.name LIKE ? OR s.name LIKE ?)
|
|
71
|
+
AND s.kind IN ('class', 'interface', 'trait', 'enum')
|
|
72
|
+
LIMIT 10
|
|
73
|
+
`;
|
|
74
|
+
const rows = this.db
|
|
75
|
+
.query<any, [string, string, string]>(symbolQuery)
|
|
76
|
+
.all(containerName, `%\\${containerName}`, `%/${containerName}`);
|
|
77
|
+
|
|
78
|
+
if (rows.length > 0) {
|
|
79
|
+
return rows.map((r) => ({
|
|
80
|
+
file: {
|
|
81
|
+
id: r.f_id,
|
|
82
|
+
domain_id: r.f_domain_id,
|
|
83
|
+
path: r.file_path,
|
|
84
|
+
archetype: r.f_archetype,
|
|
85
|
+
content_hash: r.f_content_hash,
|
|
86
|
+
line_count: r.f_line_count,
|
|
87
|
+
mtime_ms: r.f_mtime_ms,
|
|
88
|
+
size_bytes: r.f_size_bytes,
|
|
89
|
+
last_scanned_at: r.f_last_scanned_at,
|
|
90
|
+
},
|
|
91
|
+
symbol: {
|
|
92
|
+
id: r.id,
|
|
93
|
+
file_id: r.file_id,
|
|
94
|
+
name: r.name,
|
|
95
|
+
kind: r.kind,
|
|
96
|
+
signature: r.signature,
|
|
97
|
+
visibility: r.visibility,
|
|
98
|
+
line_start: r.line_start,
|
|
99
|
+
line_end: r.line_end,
|
|
100
|
+
line_count: r.line_count ?? (r.line_end - r.line_start + 1),
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fileQuery = `
|
|
106
|
+
SELECT * FROM files
|
|
107
|
+
WHERE path LIKE ? OR path LIKE ?
|
|
108
|
+
LIMIT 10
|
|
109
|
+
`;
|
|
110
|
+
const files = this.db
|
|
111
|
+
.query<FileRecord, [string, string]>(fileQuery)
|
|
112
|
+
.all(`%/${containerName}.%`, `%\\${containerName}.%`);
|
|
113
|
+
|
|
114
|
+
return files.map((file) => ({ file }));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public getAllSymbolsWithFiles(): Array<SymbolRecord & { file_path: string }> {
|
|
118
|
+
const query = `
|
|
119
|
+
SELECT s.*, f.path AS file_path
|
|
120
|
+
FROM symbols s
|
|
121
|
+
JOIN files f ON s.file_id = f.id
|
|
122
|
+
WHERE s.kind IN ('class', 'method', 'function')
|
|
123
|
+
ORDER BY s.name ASC
|
|
124
|
+
`;
|
|
125
|
+
return this.db.query<SymbolRecord & { file_path: string }, []>(query).all();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public findSymbolWithDomain(symbolName: string): {
|
|
129
|
+
symbol_name: string;
|
|
130
|
+
kind: string;
|
|
131
|
+
file_path: string;
|
|
132
|
+
domain_name: string;
|
|
133
|
+
} | null {
|
|
134
|
+
const query = `
|
|
135
|
+
SELECT
|
|
136
|
+
s.name AS symbol_name,
|
|
137
|
+
s.kind,
|
|
138
|
+
f.path AS file_path,
|
|
139
|
+
d.name AS domain_name
|
|
140
|
+
FROM symbols s
|
|
141
|
+
JOIN files f ON s.file_id = f.id
|
|
142
|
+
JOIN domains d ON f.domain_id = d.id
|
|
143
|
+
WHERE s.name = ?
|
|
144
|
+
LIMIT 1
|
|
145
|
+
`;
|
|
146
|
+
return this.db.query<any, [string]>(query).get(symbolName) ?? null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public findSymbolWithMembers(symbolName: string): {
|
|
150
|
+
symbol_name: string;
|
|
151
|
+
kind: string;
|
|
152
|
+
signature: string;
|
|
153
|
+
file_path: string;
|
|
154
|
+
domain_name: string;
|
|
155
|
+
methods: string[];
|
|
156
|
+
properties: string[];
|
|
157
|
+
} | null {
|
|
158
|
+
const rootQuery = `
|
|
159
|
+
SELECT
|
|
160
|
+
s.id AS symbol_id,
|
|
161
|
+
s.file_id,
|
|
162
|
+
s.name AS symbol_name,
|
|
163
|
+
s.kind,
|
|
164
|
+
s.signature,
|
|
165
|
+
f.path AS file_path,
|
|
166
|
+
d.name AS domain_name
|
|
167
|
+
FROM symbols s
|
|
168
|
+
JOIN files f ON s.file_id = f.id
|
|
169
|
+
JOIN domains d ON f.domain_id = d.id
|
|
170
|
+
WHERE s.name = ?
|
|
171
|
+
LIMIT 1
|
|
172
|
+
`;
|
|
173
|
+
const root = this.db.query<any, [string]>(rootQuery).get(symbolName);
|
|
174
|
+
if (!root) return null;
|
|
175
|
+
|
|
176
|
+
const membersQuery = `
|
|
177
|
+
SELECT name, kind, signature
|
|
178
|
+
FROM symbols
|
|
179
|
+
WHERE file_id = ? AND name LIKE ?
|
|
180
|
+
ORDER BY line_start ASC
|
|
181
|
+
`;
|
|
182
|
+
const members = this.db
|
|
183
|
+
.query<any, [number, string]>(membersQuery)
|
|
184
|
+
.all(root.file_id, `${symbolName}::%`);
|
|
185
|
+
|
|
186
|
+
const methods: string[] = [];
|
|
187
|
+
const properties: string[] = [];
|
|
188
|
+
|
|
189
|
+
for (const m of members) {
|
|
190
|
+
if (m.kind === "method") {
|
|
191
|
+
methods.push(m.signature);
|
|
192
|
+
} else if (m.kind === "property") {
|
|
193
|
+
properties.push(m.signature);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
symbol_name: root.symbol_name,
|
|
199
|
+
kind: root.kind,
|
|
200
|
+
signature: root.signature,
|
|
201
|
+
file_path: root.file_path,
|
|
202
|
+
domain_name: root.domain_name,
|
|
203
|
+
methods,
|
|
204
|
+
properties,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public getFileWithDomain(filePath: string): {
|
|
209
|
+
file_id: number;
|
|
210
|
+
file_path: string;
|
|
211
|
+
domain_name: string;
|
|
212
|
+
} | null {
|
|
213
|
+
const query = `
|
|
214
|
+
SELECT
|
|
215
|
+
f.id AS file_id,
|
|
216
|
+
f.path AS file_path,
|
|
217
|
+
d.name AS domain_name
|
|
218
|
+
FROM files f
|
|
219
|
+
JOIN domains d ON f.domain_id = d.id
|
|
220
|
+
WHERE f.path = ?
|
|
221
|
+
LIMIT 1
|
|
222
|
+
`;
|
|
223
|
+
return this.db.query<any, [string]>(query).get(filePath) ?? null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public getHotspotSymbols(options: {
|
|
227
|
+
minLines?: number;
|
|
228
|
+
kind?: SymbolKind;
|
|
229
|
+
domain?: string;
|
|
230
|
+
limit?: number;
|
|
231
|
+
} = {}): Array<SymbolRecord & { file_path: string; domain_name: string }> {
|
|
232
|
+
const minLines = options.minLines ?? 30;
|
|
233
|
+
const limit = options.limit ?? 50;
|
|
234
|
+
|
|
235
|
+
let sql = `
|
|
236
|
+
SELECT
|
|
237
|
+
s.id,
|
|
238
|
+
s.file_id,
|
|
239
|
+
s.name,
|
|
240
|
+
s.kind,
|
|
241
|
+
s.signature,
|
|
242
|
+
s.visibility,
|
|
243
|
+
s.line_start,
|
|
244
|
+
s.line_end,
|
|
245
|
+
s.line_count,
|
|
246
|
+
f.path AS file_path,
|
|
247
|
+
d.name AS domain_name
|
|
248
|
+
FROM symbols s
|
|
249
|
+
JOIN files f ON s.file_id = f.id
|
|
250
|
+
JOIN domains d ON f.domain_id = d.id
|
|
251
|
+
WHERE s.line_count >= ?
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
const params: any[] = [minLines];
|
|
255
|
+
|
|
256
|
+
if (options.kind) {
|
|
257
|
+
sql += " AND s.kind = ?";
|
|
258
|
+
params.push(options.kind);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (options.domain) {
|
|
262
|
+
sql += " AND d.name = ?";
|
|
263
|
+
params.push(options.domain);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
sql += " ORDER BY s.line_count DESC, s.name ASC LIMIT ?";
|
|
267
|
+
params.push(limit);
|
|
268
|
+
|
|
269
|
+
return this.db.query<any, any[]>(sql).all(...params);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public getSymbolDetails(
|
|
273
|
+
symbolQuery: string,
|
|
274
|
+
options: { domain?: string; includeDependencies?: boolean } = {}
|
|
275
|
+
): GetSymbolDetailsResponse | null {
|
|
276
|
+
const trimmed = symbolQuery.trim();
|
|
277
|
+
if (!trimmed) return null;
|
|
278
|
+
|
|
279
|
+
let sql = `
|
|
280
|
+
SELECT
|
|
281
|
+
s.id,
|
|
282
|
+
s.file_id,
|
|
283
|
+
s.name,
|
|
284
|
+
s.kind,
|
|
285
|
+
s.signature,
|
|
286
|
+
s.visibility,
|
|
287
|
+
s.line_start,
|
|
288
|
+
s.line_end,
|
|
289
|
+
s.line_count,
|
|
290
|
+
f.path AS file_path,
|
|
291
|
+
d.name AS domain_name
|
|
292
|
+
FROM symbols s
|
|
293
|
+
JOIN files f ON s.file_id = f.id
|
|
294
|
+
JOIN domains d ON f.domain_id = d.id
|
|
295
|
+
WHERE (s.name = ? OR s.name LIKE ? OR s.name LIKE ?)
|
|
296
|
+
`;
|
|
297
|
+
|
|
298
|
+
const params: any[] = [trimmed, `%::${trimmed}`, `${trimmed}::%`];
|
|
299
|
+
|
|
300
|
+
if (options.domain) {
|
|
301
|
+
sql += " AND d.name = ?";
|
|
302
|
+
params.push(options.domain);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
sql += " ORDER BY CASE WHEN s.name = ? THEN 1 WHEN s.name LIKE ? THEN 2 ELSE 3 END, s.line_start ASC LIMIT 1";
|
|
306
|
+
params.push(trimmed, `%::${trimmed}`);
|
|
307
|
+
|
|
308
|
+
const row = this.db.query<any, any[]>(sql).get(...params);
|
|
309
|
+
if (!row) return null;
|
|
310
|
+
|
|
311
|
+
// 1. Sibling methods
|
|
312
|
+
let containerName = "";
|
|
313
|
+
if (row.name.includes("::")) {
|
|
314
|
+
containerName = row.name.split("::")[0];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let siblingMethods: string[] = [];
|
|
318
|
+
if (containerName) {
|
|
319
|
+
const siblings = this.db
|
|
320
|
+
.query<{ name: string }, [number, string, string]>(
|
|
321
|
+
`SELECT name FROM symbols
|
|
322
|
+
WHERE file_id = ? AND kind = 'method' AND name != ? AND name LIKE ?
|
|
323
|
+
ORDER BY line_start ASC LIMIT 30`
|
|
324
|
+
)
|
|
325
|
+
.all(row.file_id, row.name, `${containerName}::%`);
|
|
326
|
+
siblingMethods = siblings.map((s) => s.name.replace(`${containerName}::`, ""));
|
|
327
|
+
} else if (row.kind === "class" || row.kind === "interface" || row.kind === "trait") {
|
|
328
|
+
const classMethods = this.db
|
|
329
|
+
.query<{ name: string }, [number, string]>(
|
|
330
|
+
`SELECT name FROM symbols
|
|
331
|
+
WHERE file_id = ? AND kind = 'method' AND name LIKE ?
|
|
332
|
+
ORDER BY line_start ASC LIMIT 30`
|
|
333
|
+
)
|
|
334
|
+
.all(row.file_id, `${row.name}::%`);
|
|
335
|
+
siblingMethods = classMethods.map((s) => s.name.replace(`${row.name}::`, ""));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 2. Dependencies Injected (from signature type-hints and constructor)
|
|
339
|
+
const dependenciesInjected = new Set<string>();
|
|
340
|
+
const extractTypeHints = (sig: string) => {
|
|
341
|
+
const paramMatch = sig.match(/\(([^)]*)\)/);
|
|
342
|
+
if (paramMatch && paramMatch[1]) {
|
|
343
|
+
const params = paramMatch[1].split(",");
|
|
344
|
+
const primitiveTypes = new Set([
|
|
345
|
+
"int", "string", "bool", "boolean", "float", "array", "iterable",
|
|
346
|
+
"callable", "void", "mixed", "object", "null", "never", "any", "number", "unknown"
|
|
347
|
+
]);
|
|
348
|
+
for (const p of params) {
|
|
349
|
+
const parts = p.trim().replace(/^\?/, "").split(/\s+/);
|
|
350
|
+
if (parts.length >= 2) {
|
|
351
|
+
const rawType = parts[0].replace(/\[\]$/, "").replace(/^[\\$]/, "");
|
|
352
|
+
if (rawType && !primitiveTypes.has(rawType.toLowerCase()) && !rawType.startsWith("$")) {
|
|
353
|
+
dependenciesInjected.add(rawType);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
extractTypeHints(row.signature);
|
|
361
|
+
|
|
362
|
+
// Also check constructor injection if this is a method
|
|
363
|
+
if (containerName) {
|
|
364
|
+
const ctor = this.db
|
|
365
|
+
.query<{ signature: string }, [number, string, string]>(
|
|
366
|
+
"SELECT signature FROM symbols WHERE file_id = ? AND (name = ? OR name LIKE ?) LIMIT 1"
|
|
367
|
+
)
|
|
368
|
+
.get(row.file_id, `${containerName}::__construct`, `%::__construct`);
|
|
369
|
+
if (ctor) {
|
|
370
|
+
extractTypeHints(ctor.signature);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 3. Outbound Calls (inspected from physical source file lines if accessible)
|
|
375
|
+
const outboundCalls = new Set<string>();
|
|
376
|
+
try {
|
|
377
|
+
if (fs.existsSync(row.file_path)) {
|
|
378
|
+
const fileContent = fs.readFileSync(row.file_path, "utf-8");
|
|
379
|
+
const lines = fileContent.split(/\r?\n/);
|
|
380
|
+
const start = Math.max(0, row.line_start - 1);
|
|
381
|
+
const end = Math.min(lines.length, row.line_end);
|
|
382
|
+
const sliceLines = lines.slice(start, end).join("\n");
|
|
383
|
+
|
|
384
|
+
// Match Static invocations: ClassName::methodName(
|
|
385
|
+
const staticCalls = sliceLines.matchAll(/([A-Z][a-zA-Z0-9_]+)::([a-zA-Z0-9_]+)\s*\(/g);
|
|
386
|
+
for (const m of staticCalls) {
|
|
387
|
+
if (m[1] !== "self" && m[1] !== "parent" && m[1] !== "static") {
|
|
388
|
+
outboundCalls.add(`${m[1]}::${m[2]}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Match property calls: $this->property->method(
|
|
393
|
+
const propCalls = sliceLines.matchAll(/\$this->([a-zA-Z0-9_]+)->([a-zA-Z0-9_]+)\s*\(/g);
|
|
394
|
+
for (const m of propCalls) {
|
|
395
|
+
// Capitalize property name for clean domain inference (e.g. orderService -> OrderService::cancelOrder)
|
|
396
|
+
const propName = m[1];
|
|
397
|
+
const methodName = m[2];
|
|
398
|
+
const inferredClass = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
399
|
+
outboundCalls.add(`${inferredClass}::${methodName}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
// Graceful fallback if file not physically accessible
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
symbol: row.name,
|
|
408
|
+
kind: row.kind,
|
|
409
|
+
file_path: row.file_path,
|
|
410
|
+
domain: row.domain_name,
|
|
411
|
+
exact_range: {
|
|
412
|
+
start_line: row.line_start,
|
|
413
|
+
end_line: row.line_end,
|
|
414
|
+
total_lines: row.line_count,
|
|
415
|
+
},
|
|
416
|
+
signature: row.signature,
|
|
417
|
+
visibility: row.visibility,
|
|
418
|
+
dependencies_injected: Array.from(dependenciesInjected),
|
|
419
|
+
outbound_calls: Array.from(outboundCalls),
|
|
420
|
+
sibling_methods: siblingMethods.length > 0 ? siblingMethods : undefined,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type {
|
|
3
|
+
ArchetypeKind,
|
|
4
|
+
DomainCatalogResponse,
|
|
5
|
+
DomainConfig,
|
|
6
|
+
DomainRecord,
|
|
7
|
+
ExtractedDependency,
|
|
8
|
+
ExtractedSymbol,
|
|
9
|
+
FileRecord,
|
|
10
|
+
GetSymbolDetailsResponse,
|
|
11
|
+
GetSymbolImpactResponse,
|
|
12
|
+
InboundCaller,
|
|
13
|
+
SymbolKind,
|
|
14
|
+
SymbolRecord,
|
|
15
|
+
VerticalSliceRecord,
|
|
16
|
+
} from "../../types/index.ts";
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
ArchetypeKind,
|
|
20
|
+
DomainCatalogResponse,
|
|
21
|
+
DomainConfig,
|
|
22
|
+
DomainRecord,
|
|
23
|
+
ExtractedDependency,
|
|
24
|
+
ExtractedSymbol,
|
|
25
|
+
FileRecord,
|
|
26
|
+
GetSymbolDetailsResponse,
|
|
27
|
+
GetSymbolImpactResponse,
|
|
28
|
+
InboundCaller,
|
|
29
|
+
SymbolKind,
|
|
30
|
+
SymbolRecord,
|
|
31
|
+
VerticalSliceRecord,
|
|
32
|
+
} from "../../types/index.ts";
|
|
33
|
+
import { DependencyRepository } from "./repositories/dependency.repository.ts";
|
|
34
|
+
import { DomainRepository } from "./repositories/domain.repository.ts";
|
|
35
|
+
import { FileRepository } from "./repositories/file.repository.ts";
|
|
36
|
+
import { MetaRepository } from "./repositories/meta.repository.ts";
|
|
37
|
+
import { SymbolRepository } from "./repositories/symbol.repository.ts";
|
|
38
|
+
|
|
39
|
+
export type DatabaseRepository = SeptumRepository;
|
|
40
|
+
|
|
41
|
+
export class SeptumRepository {
|
|
42
|
+
public readonly domains: DomainRepository;
|
|
43
|
+
public readonly files: FileRepository;
|
|
44
|
+
public readonly symbols: SymbolRepository;
|
|
45
|
+
public readonly dependencies: DependencyRepository;
|
|
46
|
+
public readonly meta: MetaRepository;
|
|
47
|
+
|
|
48
|
+
constructor(private db: Database) {
|
|
49
|
+
this.domains = new DomainRepository(db);
|
|
50
|
+
this.files = new FileRepository(db);
|
|
51
|
+
this.symbols = new SymbolRepository(db);
|
|
52
|
+
this.dependencies = new DependencyRepository(db);
|
|
53
|
+
this.meta = new MetaRepository(db);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public runInTransaction<T>(fn: () => T): T {
|
|
57
|
+
return this.db.transaction(fn)();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- Domain Methods ---
|
|
61
|
+
public upsertDomain(name: string, config: DomainConfig): number {
|
|
62
|
+
return this.domains.upsertDomain(name, config);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public getDomainByName(name: string): DomainRecord | null {
|
|
66
|
+
return this.domains.getDomainByName(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public getAllDomains(): DomainRecord[] {
|
|
70
|
+
return this.domains.getAllDomains();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- File Methods ---
|
|
74
|
+
public getFileByPath(path: string): FileRecord | null {
|
|
75
|
+
return this.files.getFileByPath(path);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public getFilesByDomain(domainId: number): FileRecord[] {
|
|
79
|
+
return this.files.getFilesByDomain(domainId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public upsertFile(
|
|
83
|
+
domainId: number,
|
|
84
|
+
path: string,
|
|
85
|
+
archetype: ArchetypeKind,
|
|
86
|
+
contentHash: string,
|
|
87
|
+
lineCount: number,
|
|
88
|
+
mtimeMs: number = 0,
|
|
89
|
+
sizeBytes: number = 0
|
|
90
|
+
): number {
|
|
91
|
+
return this.files.upsertFile(
|
|
92
|
+
domainId,
|
|
93
|
+
path,
|
|
94
|
+
archetype,
|
|
95
|
+
contentHash,
|
|
96
|
+
lineCount,
|
|
97
|
+
mtimeMs,
|
|
98
|
+
sizeBytes
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public updateFileMetadataOnly(id: number, mtimeMs: number, sizeBytes: number): void {
|
|
103
|
+
this.files.updateFileMetadataOnly(id, mtimeMs, sizeBytes);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public deleteFilesNotInPaths(domainId: number, validPaths: string[]): number {
|
|
107
|
+
return this.files.deleteFilesNotInPaths(domainId, validPaths);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public deleteFile(path: string): void {
|
|
111
|
+
this.files.deleteFile(path);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public getDomainCatalog(
|
|
115
|
+
domainName: string,
|
|
116
|
+
archetypeFilter?: string
|
|
117
|
+
): DomainCatalogResponse | null {
|
|
118
|
+
return this.domains.getDomainCatalog(domainName, archetypeFilter);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Symbol Methods ---
|
|
122
|
+
public replaceFileSymbols(fileId: number, symbols: ExtractedSymbol[]): void {
|
|
123
|
+
this.symbols.replaceFileSymbols(fileId, symbols);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public getSymbolsByFileId(fileId: number): SymbolRecord[] {
|
|
127
|
+
return this.symbols.getSymbolsByFileId(fileId);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public findSymbolsByName(name: string): Array<SymbolRecord & { file_path: string }> {
|
|
131
|
+
return this.symbols.findSymbolsByName(name);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public findContainers(
|
|
135
|
+
containerName: string
|
|
136
|
+
): Array<{ file: FileRecord; symbol?: SymbolRecord }> {
|
|
137
|
+
return this.symbols.findContainers(containerName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public getAllSymbolsWithFiles(): Array<SymbolRecord & { file_path: string }> {
|
|
141
|
+
return this.symbols.getAllSymbolsWithFiles();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Dependency & Slice Methods ---
|
|
145
|
+
public replaceFileDependencies(fileId: number, deps: ExtractedDependency[]): void {
|
|
146
|
+
this.dependencies.replaceFileDependencies(fileId, deps);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public upsertVerticalSlice(
|
|
150
|
+
slice: Omit<VerticalSliceRecord, "id" | "created_at">
|
|
151
|
+
): void {
|
|
152
|
+
this.dependencies.upsertVerticalSlice(slice);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public findVerticalSlices(searchTerm: string): VerticalSliceRecord[] {
|
|
156
|
+
return this.dependencies.findVerticalSlices(searchTerm);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public getAllVerticalSlices(): VerticalSliceRecord[] {
|
|
160
|
+
return this.dependencies.getAllVerticalSlices();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public clearVerticalSlices(domainId?: number): void {
|
|
164
|
+
this.dependencies.clearVerticalSlices(domainId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public getAllDependenciesWithDomains(): Array<{
|
|
168
|
+
source_file: string;
|
|
169
|
+
source_domain: string;
|
|
170
|
+
line_number: number;
|
|
171
|
+
target: string;
|
|
172
|
+
statement: string;
|
|
173
|
+
is_external: boolean;
|
|
174
|
+
}> {
|
|
175
|
+
return this.dependencies.getAllDependenciesWithDomains();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public findSymbolWithDomain(
|
|
179
|
+
symbolName: string
|
|
180
|
+
): {
|
|
181
|
+
symbol_name: string;
|
|
182
|
+
kind: string;
|
|
183
|
+
file_path: string;
|
|
184
|
+
domain_name: string;
|
|
185
|
+
} | null {
|
|
186
|
+
return this.symbols.findSymbolWithDomain(symbolName);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public findSymbolWithMembers(symbolName: string): {
|
|
190
|
+
symbol_name: string;
|
|
191
|
+
kind: string;
|
|
192
|
+
signature: string;
|
|
193
|
+
file_path: string;
|
|
194
|
+
domain_name: string;
|
|
195
|
+
methods: string[];
|
|
196
|
+
properties: string[];
|
|
197
|
+
} | null {
|
|
198
|
+
return this.symbols.findSymbolWithMembers(symbolName);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public getFileWithDomain(filePath: string): {
|
|
202
|
+
file_id: number;
|
|
203
|
+
file_path: string;
|
|
204
|
+
domain_name: string;
|
|
205
|
+
} | null {
|
|
206
|
+
return this.symbols.getFileWithDomain(filePath);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public getHotspotSymbols(options: {
|
|
210
|
+
minLines?: number;
|
|
211
|
+
kind?: SymbolKind;
|
|
212
|
+
domain?: string;
|
|
213
|
+
limit?: number;
|
|
214
|
+
} = {}): Array<SymbolRecord & { file_path: string; domain_name: string }> {
|
|
215
|
+
return this.symbols.getHotspotSymbols(options);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public getSymbolDetails(
|
|
219
|
+
symbolQuery: string,
|
|
220
|
+
options: { domain?: string; includeDependencies?: boolean } = {}
|
|
221
|
+
): GetSymbolDetailsResponse | null {
|
|
222
|
+
return this.symbols.getSymbolDetails(symbolQuery, options);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public getSymbolImpact(symbolName: string): GetSymbolImpactResponse {
|
|
226
|
+
return this.dependencies.getSymbolImpact(symbolName);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- Metadata Methods ---
|
|
230
|
+
public getMeta(key: string): string | null {
|
|
231
|
+
return this.meta.getMeta(key);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public setMeta(key: string, value: string): void {
|
|
235
|
+
this.meta.setMeta(key, value);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public getAllMeta(): Record<string, string> {
|
|
239
|
+
return this.meta.getAllMeta();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|