septum 0.1.5 → 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/package.json +1 -1
- package/src/cli/commands/hook.ts +36 -8
- package/src/cli/commands/init.ts +6 -4
- package/src/cli/commands/query.ts +45 -5
- package/src/cli/commands/slice.ts +4 -2
- package/src/cli/index.ts +15 -5
- package/src/cli/templates/hooks.ts +6 -6
- package/src/cli/templates/rules.ts +35 -23
- package/src/cli/templates/skill.ts +47 -51
- package/src/core/boundary/evaluator.ts +198 -1
- package/src/core/config/loader.ts +20 -0
- package/src/core/database/repositories/dependency.repository.ts +249 -63
- package/src/core/database/repositories/domain.repository.ts +96 -39
- package/src/core/database/repositories/file.repository.ts +20 -19
- package/src/core/database/repositories/symbol.repository.ts +44 -5
- package/src/core/database/repository.ts +19 -2
- package/src/core/database/schema.ts +3 -2
- package/src/core/ingestion/pipeline.ts +47 -25
- package/src/core/parser/boundary-tracker.ts +145 -0
- package/src/core/parser/extractors/frontend.ts +43 -90
- package/src/core/parser/extractors/go.ts +200 -21
- package/src/core/parser/extractors/laravel-semantic.ts +18 -12
- package/src/core/parser/extractors/mcp-cli-semantic.ts +173 -44
- package/src/core/parser/extractors/nestjs-semantic.ts +20 -5
- package/src/core/parser/extractors/php.ts +293 -31
- package/src/core/parser/extractors/python.ts +242 -10
- package/src/core/parser/extractors/typescript.ts +604 -117
- package/src/core/parser/wasm-loader.ts +271 -0
- package/src/core/resolver/call-graph-tracer.ts +49 -17
- package/src/core/resolver/module-resolver.ts +87 -16
- package/src/core/resolver/vertical-slice-tracer.ts +33 -11
- package/src/core/topology/environment-detector.ts +109 -6
- package/src/mcp/schemas.ts +146 -29
- package/src/mcp/server.ts +93 -41
- package/src/mcp/tools/check-boundary.ts +176 -40
- package/src/mcp/tools/get-domain-catalog.ts +24 -14
- package/src/mcp/tools/get-environment-topology.ts +3 -6
- package/src/mcp/tools/get-feature-context.ts +35 -20
- package/src/mcp/tools/get-symbol-hotspots.ts +4 -10
- package/src/mcp/tools/locate-symbol.ts +12 -14
- package/src/mcp/tools/register-domain.ts +3 -12
- package/src/mcp/tools/register-feature.ts +22 -31
- package/src/mcp/tools/trace-vertical-slice.ts +8 -8
- package/src/types/index.ts +36 -5
|
@@ -7,6 +7,15 @@ import type {
|
|
|
7
7
|
SymbolKind,
|
|
8
8
|
SymbolRecord,
|
|
9
9
|
} from "../../../types/index.ts";
|
|
10
|
+
import { DependencyRepository } from "./dependency.repository.ts";
|
|
11
|
+
|
|
12
|
+
const CONTAINER_KINDS = new Set<SymbolKind>([
|
|
13
|
+
"class",
|
|
14
|
+
"interface",
|
|
15
|
+
"trait",
|
|
16
|
+
"struct",
|
|
17
|
+
"enum",
|
|
18
|
+
]);
|
|
10
19
|
|
|
11
20
|
export class DomainRepository {
|
|
12
21
|
constructor(private db: Database) {}
|
|
@@ -70,11 +79,15 @@ export class DomainRepository {
|
|
|
70
79
|
|
|
71
80
|
public getDomainCatalog(
|
|
72
81
|
domainName: string,
|
|
73
|
-
archetypeFilter?: string
|
|
82
|
+
archetypeFilter?: string,
|
|
83
|
+
options?: { detailLevel?: "compact" | "full"; fileFilter?: string }
|
|
74
84
|
): DomainCatalogResponse | null {
|
|
75
85
|
const domain = this.getDomainByName(domainName);
|
|
76
86
|
if (!domain) return null;
|
|
77
87
|
|
|
88
|
+
const detailLevel = options?.detailLevel ?? "compact";
|
|
89
|
+
const fileFilter = options?.fileFilter?.trim();
|
|
90
|
+
|
|
78
91
|
const allowedDeps: string[] = JSON.parse(domain.allowed_deps_json || "[]");
|
|
79
92
|
const forbiddenDeps: string[] = JSON.parse(domain.forbidden_deps_json || "[]");
|
|
80
93
|
|
|
@@ -87,7 +100,14 @@ export class DomainRepository {
|
|
|
87
100
|
}
|
|
88
101
|
filesQuery += " ORDER BY path ASC";
|
|
89
102
|
|
|
90
|
-
const
|
|
103
|
+
const allDomainFiles = this.db.query<FileRecord, any>(filesQuery).all(...params);
|
|
104
|
+
const totalFiles = allDomainFiles.length;
|
|
105
|
+
|
|
106
|
+
const files = fileFilter
|
|
107
|
+
? allDomainFiles.filter((f) =>
|
|
108
|
+
f.path.replace(/\\/g, "/").toLowerCase().includes(fileFilter.replace(/\\/g, "/").toLowerCase())
|
|
109
|
+
)
|
|
110
|
+
: allDomainFiles;
|
|
91
111
|
|
|
92
112
|
const catalogFiles: DomainCatalogResponse["files"] = [];
|
|
93
113
|
|
|
@@ -115,6 +135,10 @@ export class DomainRepository {
|
|
|
115
135
|
}
|
|
116
136
|
}
|
|
117
137
|
|
|
138
|
+
// Batch compute file connections for the domain (zero N+1)
|
|
139
|
+
const depRepo = new DependencyRepository(this.db);
|
|
140
|
+
const connectionsByPath = depRepo.getDomainFileConnections(domain.id);
|
|
141
|
+
|
|
118
142
|
for (const file of files) {
|
|
119
143
|
const symbols = symbolsByFileId.get(file.id) ?? [];
|
|
120
144
|
|
|
@@ -123,6 +147,7 @@ export class DomainRepository {
|
|
|
123
147
|
{
|
|
124
148
|
name: string;
|
|
125
149
|
kind: SymbolKind;
|
|
150
|
+
lineSpan: string;
|
|
126
151
|
properties: string[];
|
|
127
152
|
methods: string[];
|
|
128
153
|
signatures: string[];
|
|
@@ -130,63 +155,66 @@ export class DomainRepository {
|
|
|
130
155
|
>();
|
|
131
156
|
|
|
132
157
|
for (const sym of symbols) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
sym.kind === "interface" ||
|
|
136
|
-
sym.kind === "trait" ||
|
|
137
|
-
sym.kind === "enum"
|
|
138
|
-
) {
|
|
158
|
+
const lineSpan = `${sym.line_start}-${sym.line_end}`;
|
|
159
|
+
if (CONTAINER_KINDS.has(sym.kind)) {
|
|
139
160
|
if (!symbolMap.has(sym.name)) {
|
|
140
161
|
symbolMap.set(sym.name, {
|
|
141
162
|
name: sym.name,
|
|
142
163
|
kind: sym.kind,
|
|
164
|
+
lineSpan,
|
|
143
165
|
properties: [],
|
|
144
166
|
methods: [],
|
|
145
167
|
signatures: [sym.signature],
|
|
146
168
|
});
|
|
147
169
|
}
|
|
148
|
-
} else if (sym.kind === "method") {
|
|
149
|
-
let targetContainer
|
|
170
|
+
} else if (sym.kind === "method" || sym.kind === "property") {
|
|
171
|
+
let targetContainer:
|
|
172
|
+
| {
|
|
173
|
+
name: string;
|
|
174
|
+
kind: SymbolKind;
|
|
175
|
+
lineSpan: string;
|
|
176
|
+
properties: string[];
|
|
177
|
+
methods: string[];
|
|
178
|
+
signatures: string[];
|
|
179
|
+
}
|
|
180
|
+
| undefined;
|
|
181
|
+
|
|
150
182
|
if (sym.name.includes("::")) {
|
|
151
183
|
const containerName = sym.name.split("::")[0];
|
|
152
|
-
|
|
153
|
-
|
|
184
|
+
const candidate = symbolMap.get(containerName);
|
|
185
|
+
if (candidate && CONTAINER_KINDS.has(candidate.kind)) {
|
|
186
|
+
targetContainer = candidate;
|
|
154
187
|
}
|
|
155
188
|
}
|
|
189
|
+
|
|
156
190
|
if (targetContainer) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
kind: sym.kind,
|
|
162
|
-
properties: [],
|
|
163
|
-
methods: [],
|
|
164
|
-
signatures: [sym.signature],
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
} else if (sym.kind === "property") {
|
|
168
|
-
let targetContainer = Array.from(symbolMap.values()).pop();
|
|
169
|
-
if (sym.name.includes("::")) {
|
|
170
|
-
const containerName = sym.name.split("::")[0];
|
|
171
|
-
if (symbolMap.has(containerName)) {
|
|
172
|
-
targetContainer = symbolMap.get(containerName);
|
|
191
|
+
if (sym.kind === "method") {
|
|
192
|
+
targetContainer.methods.push(sym.signature);
|
|
193
|
+
} else {
|
|
194
|
+
targetContainer.properties.push(sym.signature);
|
|
173
195
|
}
|
|
174
|
-
}
|
|
175
|
-
if (targetContainer) {
|
|
176
|
-
targetContainer.properties.push(sym.signature);
|
|
177
196
|
} else {
|
|
178
197
|
symbolMap.set(sym.name, {
|
|
179
198
|
name: sym.name,
|
|
180
199
|
kind: sym.kind,
|
|
181
|
-
|
|
200
|
+
lineSpan,
|
|
201
|
+
properties: sym.kind === "property" ? [sym.signature] : [],
|
|
182
202
|
methods: [],
|
|
183
203
|
signatures: [sym.signature],
|
|
184
204
|
});
|
|
185
205
|
}
|
|
186
206
|
} else {
|
|
207
|
+
if (
|
|
208
|
+
detailLevel === "compact" &&
|
|
209
|
+
(sym.kind === "variable" || sym.kind === "constant") &&
|
|
210
|
+
sym.visibility === "private"
|
|
211
|
+
) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
187
214
|
symbolMap.set(sym.name, {
|
|
188
215
|
name: sym.name,
|
|
189
216
|
kind: sym.kind,
|
|
217
|
+
lineSpan,
|
|
190
218
|
properties: [],
|
|
191
219
|
methods: [],
|
|
192
220
|
signatures: [sym.signature],
|
|
@@ -194,25 +222,54 @@ export class DomainRepository {
|
|
|
194
222
|
}
|
|
195
223
|
}
|
|
196
224
|
|
|
225
|
+
const rawConn = connectionsByPath.get(file.path);
|
|
226
|
+
const fileConnections = {
|
|
227
|
+
imports_from: rawConn?.imports_from ?? [],
|
|
228
|
+
imported_by_count: rawConn?.imported_by_count ?? 0,
|
|
229
|
+
};
|
|
230
|
+
|
|
197
231
|
catalogFiles.push({
|
|
198
232
|
path: file.path,
|
|
199
233
|
archetype: file.archetype,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
234
|
+
line_count: file.line_count,
|
|
235
|
+
connections: fileConnections,
|
|
236
|
+
symbols: Array.from(symbolMap.values()).map((s) => {
|
|
237
|
+
if (detailLevel === "compact") {
|
|
238
|
+
return {
|
|
239
|
+
name: s.name,
|
|
240
|
+
kind: s.kind,
|
|
241
|
+
line_span: s.lineSpan,
|
|
242
|
+
methods_count: s.methods.length > 0 ? s.methods.length : undefined,
|
|
243
|
+
properties_count: s.properties.length > 0 ? s.properties.length : undefined,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
name: s.name,
|
|
248
|
+
kind: s.kind,
|
|
249
|
+
line_span: s.lineSpan,
|
|
250
|
+
methods_count: s.methods.length > 0 ? s.methods.length : undefined,
|
|
251
|
+
properties_count: s.properties.length > 0 ? s.properties.length : undefined,
|
|
252
|
+
properties: s.properties.length > 0 ? s.properties : undefined,
|
|
253
|
+
methods: s.methods.length > 0 ? s.methods : undefined,
|
|
254
|
+
signatures: s.signatures.length > 0 ? s.signatures : undefined,
|
|
255
|
+
};
|
|
256
|
+
}),
|
|
207
257
|
});
|
|
208
258
|
}
|
|
209
259
|
|
|
210
260
|
return {
|
|
211
261
|
domain: domain.name,
|
|
212
262
|
root: domain.root_path,
|
|
263
|
+
detail_level: detailLevel,
|
|
264
|
+
total_files: totalFiles,
|
|
265
|
+
showing_files: catalogFiles.length,
|
|
213
266
|
allowed_dependencies: allowedDeps,
|
|
214
267
|
forbidden_dependencies: forbiddenDeps,
|
|
215
268
|
files: catalogFiles,
|
|
269
|
+
agent_hint:
|
|
270
|
+
detailLevel === "compact"
|
|
271
|
+
? "Compact view (<400 tokens). Use septum_get_symbol({ symbol }) for exact method signatures, or septum_get_domain_catalog({ domain, detail_level: 'full', file_filter: '<path>' }) for full member lists."
|
|
272
|
+
: undefined,
|
|
216
273
|
};
|
|
217
274
|
}
|
|
218
275
|
}
|
|
@@ -44,25 +44,22 @@ export class FileRepository {
|
|
|
44
44
|
mtimeMs: number = 0,
|
|
45
45
|
sizeBytes: number = 0
|
|
46
46
|
): number {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.run(domainId, path, archetype, contentHash, lineCount, mtimeMs, sizeBytes);
|
|
64
|
-
return Number(result.lastInsertRowid);
|
|
65
|
-
}
|
|
47
|
+
const row = this.db
|
|
48
|
+
.query<{ id: number }, [number, string, string, string, number, number, number]>(
|
|
49
|
+
`INSERT INTO files (domain_id, path, archetype, content_hash, line_count, mtime_ms, size_bytes)
|
|
50
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
51
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
52
|
+
domain_id = excluded.domain_id,
|
|
53
|
+
archetype = excluded.archetype,
|
|
54
|
+
content_hash = excluded.content_hash,
|
|
55
|
+
line_count = excluded.line_count,
|
|
56
|
+
mtime_ms = excluded.mtime_ms,
|
|
57
|
+
size_bytes = excluded.size_bytes,
|
|
58
|
+
last_scanned_at = CURRENT_TIMESTAMP
|
|
59
|
+
RETURNING id`
|
|
60
|
+
)
|
|
61
|
+
.get(domainId, path, archetype, contentHash, lineCount, mtimeMs, sizeBytes);
|
|
62
|
+
return row ? Number(row.id) : 0;
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
public updateFileMetadataOnly(id: number, mtimeMs: number, sizeBytes: number): void {
|
|
@@ -100,6 +97,10 @@ export class FileRepository {
|
|
|
100
97
|
this.db.query("DELETE FROM files WHERE path = ?").run(path);
|
|
101
98
|
}
|
|
102
99
|
|
|
100
|
+
public deleteFileByPath(path: string): void {
|
|
101
|
+
this.deleteFile(path);
|
|
102
|
+
}
|
|
103
|
+
|
|
103
104
|
public getFileWithDomain(
|
|
104
105
|
filePath: string
|
|
105
106
|
): { file: FileRecord; domain: DomainRecord } | null {
|
|
@@ -8,6 +8,26 @@ import type {
|
|
|
8
8
|
SymbolKind,
|
|
9
9
|
SymbolRecord,
|
|
10
10
|
} from "../../../types/index.ts";
|
|
11
|
+
import { DependencyRepository } from "./dependency.repository.ts";
|
|
12
|
+
|
|
13
|
+
const SQL_KEYWORD_BLACKLIST = new Set([
|
|
14
|
+
"VALUES",
|
|
15
|
+
"WHERE",
|
|
16
|
+
"SELECT",
|
|
17
|
+
"FROM",
|
|
18
|
+
"JOIN",
|
|
19
|
+
"LIKE",
|
|
20
|
+
"INSERT",
|
|
21
|
+
"UPDATE",
|
|
22
|
+
"DELETE",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function stripCommentsAndStrings(source: string): string {
|
|
26
|
+
return source
|
|
27
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
28
|
+
.replace(/\/\/.*$/gm, " ")
|
|
29
|
+
.replace(/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`/g, '""');
|
|
30
|
+
}
|
|
11
31
|
|
|
12
32
|
export class SymbolRepository {
|
|
13
33
|
constructor(private db: Database) {}
|
|
@@ -354,6 +374,10 @@ export class SymbolRepository {
|
|
|
354
374
|
siblingMethods = classMethods.map((s) => s.name.replace(`${row.name}::`, ""));
|
|
355
375
|
}
|
|
356
376
|
|
|
377
|
+
siblingMethods = Array.from(new Set(siblingMethods)).filter(
|
|
378
|
+
(m) => !SQL_KEYWORD_BLACKLIST.has(m.toUpperCase())
|
|
379
|
+
);
|
|
380
|
+
|
|
357
381
|
// 2. Dependencies Injected (from signature type-hints and constructor)
|
|
358
382
|
const dependenciesInjected = new Set<string>();
|
|
359
383
|
const extractTypeHints = (sig: string) => {
|
|
@@ -398,30 +422,44 @@ export class SymbolRepository {
|
|
|
398
422
|
const lines = fileContent.split(/\r?\n/);
|
|
399
423
|
const start = Math.max(0, row.line_start - 1);
|
|
400
424
|
const end = Math.min(lines.length, row.line_end);
|
|
401
|
-
const
|
|
425
|
+
const rawSlice = lines.slice(start, end).join("\n");
|
|
426
|
+
const sanitizedSlice = stripCommentsAndStrings(rawSlice);
|
|
402
427
|
|
|
403
428
|
// Match Static invocations: ClassName::methodName(
|
|
404
|
-
const staticCalls =
|
|
429
|
+
const staticCalls = sanitizedSlice.matchAll(/([A-Z][a-zA-Z0-9_]+)::([a-zA-Z0-9_]+)\s*\(/g);
|
|
405
430
|
for (const m of staticCalls) {
|
|
406
431
|
if (m[1] !== "self" && m[1] !== "parent" && m[1] !== "static") {
|
|
407
432
|
outboundCalls.add(`${m[1]}::${m[2]}`);
|
|
408
433
|
}
|
|
409
434
|
}
|
|
410
435
|
|
|
411
|
-
// Match property calls: $this->property->method(
|
|
412
|
-
const propCalls =
|
|
436
|
+
// Match PHP property calls: $this->property->method(
|
|
437
|
+
const propCalls = sanitizedSlice.matchAll(/\$this->([a-zA-Z0-9_]+)->([a-zA-Z0-9_]+)\s*\(/g);
|
|
413
438
|
for (const m of propCalls) {
|
|
414
|
-
// Capitalize property name for clean domain inference (e.g. orderService -> OrderService::cancelOrder)
|
|
415
439
|
const propName = m[1];
|
|
416
440
|
const methodName = m[2];
|
|
417
441
|
const inferredClass = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
418
442
|
outboundCalls.add(`${inferredClass}::${methodName}`);
|
|
419
443
|
}
|
|
444
|
+
|
|
445
|
+
// Match TS/JS property calls: this.property.method(
|
|
446
|
+
const tsPropCalls = sanitizedSlice.matchAll(/\bthis\.([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\s*\(/g);
|
|
447
|
+
for (const m of tsPropCalls) {
|
|
448
|
+
const propName = m[1];
|
|
449
|
+
const methodName = m[2];
|
|
450
|
+
if (propName !== "db") {
|
|
451
|
+
const inferredClass = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
452
|
+
outboundCalls.add(`${inferredClass}::${methodName}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
420
455
|
}
|
|
421
456
|
} catch {
|
|
422
457
|
// Graceful fallback if file not physically accessible
|
|
423
458
|
}
|
|
424
459
|
|
|
460
|
+
const depRepo = new DependencyRepository(this.db);
|
|
461
|
+
const connectedFiles = depRepo.getFileConnections(row.file_path);
|
|
462
|
+
|
|
425
463
|
return {
|
|
426
464
|
symbol: row.name,
|
|
427
465
|
kind: row.kind,
|
|
@@ -437,6 +475,7 @@ export class SymbolRepository {
|
|
|
437
475
|
dependencies_injected: Array.from(dependenciesInjected),
|
|
438
476
|
outbound_calls: Array.from(outboundCalls),
|
|
439
477
|
sibling_methods: siblingMethods.length > 0 ? siblingMethods : undefined,
|
|
478
|
+
connected_files: connectedFiles,
|
|
440
479
|
};
|
|
441
480
|
}
|
|
442
481
|
|
|
@@ -122,11 +122,16 @@ export class SeptumRepository {
|
|
|
122
122
|
this.files.deleteFile(path);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
public deleteFileByPath(path: string): void {
|
|
126
|
+
this.files.deleteFileByPath(path);
|
|
127
|
+
}
|
|
128
|
+
|
|
125
129
|
public getDomainCatalog(
|
|
126
130
|
domainName: string,
|
|
127
|
-
archetypeFilter?: string
|
|
131
|
+
archetypeFilter?: string,
|
|
132
|
+
options?: { detailLevel?: "compact" | "full"; fileFilter?: string }
|
|
128
133
|
): DomainCatalogResponse | null {
|
|
129
|
-
return this.domains.getDomainCatalog(domainName, archetypeFilter);
|
|
134
|
+
return this.domains.getDomainCatalog(domainName, archetypeFilter, options);
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
// --- Symbol Methods ---
|
|
@@ -171,10 +176,22 @@ export class SeptumRepository {
|
|
|
171
176
|
return this.dependencies.getAllVerticalSlices();
|
|
172
177
|
}
|
|
173
178
|
|
|
179
|
+
public countVerticalSlices(): number {
|
|
180
|
+
return this.dependencies.countVerticalSlices();
|
|
181
|
+
}
|
|
182
|
+
|
|
174
183
|
public clearVerticalSlices(domainId?: number): void {
|
|
175
184
|
this.dependencies.clearVerticalSlices(domainId);
|
|
176
185
|
}
|
|
177
186
|
|
|
187
|
+
public getDomainFileConnections(domainId?: number) {
|
|
188
|
+
return this.dependencies.getDomainFileConnections(domainId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public getFileConnections(filePath: string) {
|
|
192
|
+
return this.dependencies.getFileConnections(filePath);
|
|
193
|
+
}
|
|
194
|
+
|
|
178
195
|
public getAllDependenciesWithDomains(): Array<{
|
|
179
196
|
source_file: string;
|
|
180
197
|
source_domain: string;
|
|
@@ -104,9 +104,10 @@ export const TABLE_STATEMENTS = [
|
|
|
104
104
|
|
|
105
105
|
export const INDEX_STATEMENTS = [
|
|
106
106
|
"CREATE INDEX IF NOT EXISTS idx_domains_name ON domains(name);",
|
|
107
|
-
"CREATE INDEX IF NOT EXISTS idx_files_domain_id ON files(domain_id);",
|
|
107
|
+
"CREATE INDEX IF NOT EXISTS idx_files_domain_id ON files(domain_id, id);",
|
|
108
108
|
"CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);",
|
|
109
109
|
"CREATE INDEX IF NOT EXISTS idx_symbols_file_id ON symbols(file_id);",
|
|
110
|
+
"CREATE INDEX IF NOT EXISTS idx_symbols_file_lines ON symbols(file_id, line_start, line_end);",
|
|
110
111
|
"CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);",
|
|
111
112
|
"CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);",
|
|
112
113
|
"CREATE INDEX IF NOT EXISTS idx_symbols_line_count ON symbols(kind, line_count DESC);",
|
|
@@ -117,7 +118,7 @@ export const INDEX_STATEMENTS = [
|
|
|
117
118
|
"CREATE INDEX IF NOT EXISTS idx_slices_route_name ON vertical_slices(route_name);",
|
|
118
119
|
"CREATE INDEX IF NOT EXISTS idx_slices_controller ON vertical_slices(controller_class, action_name);",
|
|
119
120
|
"CREATE INDEX IF NOT EXISTS idx_env_topology_status ON environment_topology(status);",
|
|
120
|
-
"CREATE INDEX IF NOT EXISTS idx_env_nodes_layer ON environment_nodes(layer);",
|
|
121
|
+
"CREATE INDEX IF NOT EXISTS idx_env_nodes_layer ON environment_nodes(layer, id);",
|
|
121
122
|
"CREATE INDEX IF NOT EXISTS idx_env_nodes_status ON environment_nodes(status);",
|
|
122
123
|
];
|
|
123
124
|
|
|
@@ -59,6 +59,11 @@ export class IngestionPipeline {
|
|
|
59
59
|
domainNames = Object.keys(config.domains);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const existingFilesMap = new Map<string, import("../../types/index.ts").FileRecord>();
|
|
63
|
+
for (const f of this.repo.getAllFiles()) {
|
|
64
|
+
existingFilesMap.set(f.path, f);
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
for (const domainName of domainNames) {
|
|
63
68
|
const domainCfg = config.domains[domainName];
|
|
64
69
|
const domainId = this.repo.upsertDomain(domainName, domainCfg);
|
|
@@ -85,7 +90,13 @@ export class IngestionPipeline {
|
|
|
85
90
|
config.settings.ignore_patterns,
|
|
86
91
|
childDomainRoots
|
|
87
92
|
);
|
|
88
|
-
const activeRelPaths
|
|
93
|
+
const activeRelPaths = new Set<string>();
|
|
94
|
+
|
|
95
|
+
const pendingMetadataUpdates: Array<{
|
|
96
|
+
id: number;
|
|
97
|
+
mtimeMs: number;
|
|
98
|
+
sizeBytes: number;
|
|
99
|
+
}> = [];
|
|
89
100
|
|
|
90
101
|
const pendingUpdates: Array<{
|
|
91
102
|
fullPath: string;
|
|
@@ -100,7 +111,7 @@ export class IngestionPipeline {
|
|
|
100
111
|
|
|
101
112
|
for (const { fullPath, relPath } of collectedFiles) {
|
|
102
113
|
filesScanned++;
|
|
103
|
-
activeRelPaths.
|
|
114
|
+
activeRelPaths.add(relPath);
|
|
104
115
|
|
|
105
116
|
let stat;
|
|
106
117
|
try {
|
|
@@ -111,7 +122,7 @@ export class IngestionPipeline {
|
|
|
111
122
|
|
|
112
123
|
const mtimeMs = Math.round(stat.mtimeMs);
|
|
113
124
|
const sizeBytes = stat.size;
|
|
114
|
-
const existingFile =
|
|
125
|
+
const existingFile = existingFilesMap.get(relPath);
|
|
115
126
|
|
|
116
127
|
// Tier 1: Fast OS metadata check (inode mtime + size + domain match)
|
|
117
128
|
if (
|
|
@@ -133,8 +144,8 @@ export class IngestionPipeline {
|
|
|
133
144
|
existingFile.domain_id === domainId &&
|
|
134
145
|
existingFile.content_hash === currentHash
|
|
135
146
|
) {
|
|
136
|
-
// Content identical despite mtime bump (e.g. touch) -
|
|
137
|
-
|
|
147
|
+
// Content identical despite mtime bump (e.g. touch) - queue metadata update for batch transaction
|
|
148
|
+
pendingMetadataUpdates.push({ id: existingFile.id, mtimeMs, sizeBytes });
|
|
138
149
|
filesSkipped++;
|
|
139
150
|
continue;
|
|
140
151
|
}
|
|
@@ -158,6 +169,10 @@ export class IngestionPipeline {
|
|
|
158
169
|
|
|
159
170
|
// Batch ACID Transaction per domain
|
|
160
171
|
this.repo.runInTransaction(() => {
|
|
172
|
+
for (const meta of pendingMetadataUpdates) {
|
|
173
|
+
this.repo.updateFileMetadataOnly(meta.id, meta.mtimeMs, meta.sizeBytes);
|
|
174
|
+
}
|
|
175
|
+
|
|
161
176
|
for (const item of pendingUpdates) {
|
|
162
177
|
const fileId = this.repo.upsertFile(
|
|
163
178
|
domainId,
|
|
@@ -176,8 +191,13 @@ export class IngestionPipeline {
|
|
|
176
191
|
dependenciesIndexed += item.ast.dependencies.length;
|
|
177
192
|
}
|
|
178
193
|
|
|
179
|
-
// State Reconciliation: Prune deleted files from DB
|
|
180
|
-
|
|
194
|
+
// State Reconciliation: Prune deleted files from DB using pre-loaded existingFilesMap
|
|
195
|
+
for (const [existingPath, existingRecord] of existingFilesMap.entries()) {
|
|
196
|
+
if (existingRecord.domain_id === domainId && !activeRelPaths.has(existingPath)) {
|
|
197
|
+
this.repo.deleteFileByPath(existingPath);
|
|
198
|
+
existingFilesMap.delete(existingPath);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
181
201
|
});
|
|
182
202
|
}
|
|
183
203
|
|
|
@@ -188,29 +208,28 @@ export class IngestionPipeline {
|
|
|
188
208
|
detectedTopology.framework
|
|
189
209
|
);
|
|
190
210
|
|
|
191
|
-
|
|
211
|
+
const slicesToPersist =
|
|
212
|
+
semanticSlices.length > 0
|
|
213
|
+
? semanticSlices
|
|
214
|
+
: new CallGraphTracer(this.repo, projectRoot).traceAllSlices();
|
|
192
215
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
// Heuristic Call-Graph Tracer fallback for Clean/Hexagonal or generic architectures
|
|
199
|
-
const callGraphTracer = new CallGraphTracer(this.repo, projectRoot);
|
|
200
|
-
const graphSlices = callGraphTracer.traceAllSlices();
|
|
201
|
-
for (const slice of graphSlices) {
|
|
216
|
+
this.repo.runInTransaction(() => {
|
|
217
|
+
this.repo.clearVerticalSlices();
|
|
218
|
+
for (const slice of slicesToPersist) {
|
|
202
219
|
this.repo.upsertVerticalSlice(slice);
|
|
203
220
|
}
|
|
204
|
-
}
|
|
221
|
+
});
|
|
205
222
|
|
|
206
223
|
const durationMs = Math.round(performance.now() - startTime);
|
|
207
224
|
|
|
208
225
|
// Update SSOT repo_meta watermark
|
|
209
|
-
this.repo.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
this.repo.runInTransaction(() => {
|
|
227
|
+
this.repo.setMeta("last_sync", new Date().toISOString());
|
|
228
|
+
this.repo.setMeta("files_scanned", String(filesScanned));
|
|
229
|
+
this.repo.setMeta("files_updated", String(filesUpdated));
|
|
230
|
+
this.repo.setMeta("symbols_indexed", String(symbolsIndexed));
|
|
231
|
+
this.repo.setMeta("dependencies_indexed", String(dependenciesIndexed));
|
|
232
|
+
});
|
|
214
233
|
|
|
215
234
|
return {
|
|
216
235
|
domains_processed: domainNames.length,
|
|
@@ -521,6 +540,10 @@ export class IngestionPipeline {
|
|
|
521
540
|
const projectRoot = this.customWorkspaceRoot || process.cwd();
|
|
522
541
|
const relPath = relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
523
542
|
|
|
543
|
+
if (this.shouldIgnore(relPath, config.settings.ignore_patterns ?? [])) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
|
|
524
547
|
let targetDomainName: string | null = null;
|
|
525
548
|
let targetDomainCfg = null;
|
|
526
549
|
|
|
@@ -535,8 +558,7 @@ export class IngestionPipeline {
|
|
|
535
558
|
}
|
|
536
559
|
|
|
537
560
|
if (!targetDomainName || !targetDomainCfg) {
|
|
538
|
-
|
|
539
|
-
targetDomainCfg = { root: ".", allowed_dependencies: [], forbidden_dependencies: [], archetypes: {} };
|
|
561
|
+
return false;
|
|
540
562
|
}
|
|
541
563
|
|
|
542
564
|
const domainId = this.repo.upsertDomain(targetDomainName, targetDomainCfg);
|