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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/cli/commands/hook.ts +36 -8
  3. package/src/cli/commands/init.ts +6 -4
  4. package/src/cli/commands/query.ts +45 -5
  5. package/src/cli/commands/slice.ts +4 -2
  6. package/src/cli/index.ts +15 -5
  7. package/src/cli/templates/hooks.ts +6 -6
  8. package/src/cli/templates/rules.ts +35 -23
  9. package/src/cli/templates/skill.ts +47 -51
  10. package/src/core/boundary/evaluator.ts +198 -1
  11. package/src/core/config/loader.ts +20 -0
  12. package/src/core/database/repositories/dependency.repository.ts +249 -63
  13. package/src/core/database/repositories/domain.repository.ts +96 -39
  14. package/src/core/database/repositories/file.repository.ts +20 -19
  15. package/src/core/database/repositories/symbol.repository.ts +44 -5
  16. package/src/core/database/repository.ts +19 -2
  17. package/src/core/database/schema.ts +3 -2
  18. package/src/core/ingestion/pipeline.ts +47 -25
  19. package/src/core/parser/boundary-tracker.ts +145 -0
  20. package/src/core/parser/extractors/frontend.ts +43 -90
  21. package/src/core/parser/extractors/go.ts +200 -21
  22. package/src/core/parser/extractors/laravel-semantic.ts +18 -12
  23. package/src/core/parser/extractors/mcp-cli-semantic.ts +173 -44
  24. package/src/core/parser/extractors/nestjs-semantic.ts +20 -5
  25. package/src/core/parser/extractors/php.ts +293 -31
  26. package/src/core/parser/extractors/python.ts +242 -10
  27. package/src/core/parser/extractors/typescript.ts +604 -117
  28. package/src/core/parser/wasm-loader.ts +271 -0
  29. package/src/core/resolver/call-graph-tracer.ts +49 -17
  30. package/src/core/resolver/module-resolver.ts +87 -16
  31. package/src/core/resolver/vertical-slice-tracer.ts +33 -11
  32. package/src/core/topology/environment-detector.ts +109 -6
  33. package/src/mcp/schemas.ts +146 -29
  34. package/src/mcp/server.ts +93 -41
  35. package/src/mcp/tools/check-boundary.ts +176 -40
  36. package/src/mcp/tools/get-domain-catalog.ts +24 -14
  37. package/src/mcp/tools/get-environment-topology.ts +3 -6
  38. package/src/mcp/tools/get-feature-context.ts +35 -20
  39. package/src/mcp/tools/get-symbol-hotspots.ts +4 -10
  40. package/src/mcp/tools/locate-symbol.ts +12 -14
  41. package/src/mcp/tools/register-domain.ts +3 -12
  42. package/src/mcp/tools/register-feature.ts +22 -31
  43. package/src/mcp/tools/trace-vertical-slice.ts +8 -8
  44. 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 files = this.db.query<FileRecord, any>(filesQuery).all(...params);
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
- if (
134
- sym.kind === "class" ||
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 = Array.from(symbolMap.values()).pop();
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
- if (symbolMap.has(containerName)) {
153
- targetContainer = symbolMap.get(containerName);
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
- targetContainer.methods.push(sym.signature);
158
- } else {
159
- symbolMap.set(sym.name, {
160
- name: sym.name,
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
- properties: [sym.signature],
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
- symbols: Array.from(symbolMap.values()).map((s) => ({
201
- name: s.name,
202
- kind: s.kind,
203
- properties: s.properties.length > 0 ? s.properties : undefined,
204
- methods: s.methods.length > 0 ? s.methods : undefined,
205
- signatures: s.signatures.length > 0 ? s.signatures : undefined,
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 existing = this.getFileByPath(path);
48
- if (existing) {
49
- this.db
50
- .query(
51
- `UPDATE files
52
- SET domain_id = ?, archetype = ?, content_hash = ?, line_count = ?, mtime_ms = ?, size_bytes = ?, last_scanned_at = CURRENT_TIMESTAMP
53
- WHERE id = ?`
54
- )
55
- .run(domainId, archetype, contentHash, lineCount, mtimeMs, sizeBytes, existing.id);
56
- return existing.id;
57
- } else {
58
- const result = this.db
59
- .query(
60
- `INSERT INTO files (domain_id, path, archetype, content_hash, line_count, mtime_ms, size_bytes)
61
- VALUES (?, ?, ?, ?, ?, ?, ?)`
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 sliceLines = lines.slice(start, end).join("\n");
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 = sliceLines.matchAll(/([A-Z][a-zA-Z0-9_]+)::([a-zA-Z0-9_]+)\s*\(/g);
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 = sliceLines.matchAll(/\$this->([a-zA-Z0-9_]+)->([a-zA-Z0-9_]+)\s*\(/g);
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: string[] = [];
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.push(relPath);
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 = this.repo.getFileByPath(relPath);
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) - update metadata without re-parsing AST
137
- this.repo.updateFileMetadataOnly(existingFile.id, mtimeMs, sizeBytes);
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
- this.repo.deleteFilesNotInPaths(domainId, activeRelPaths);
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
- this.repo.clearVerticalSlices();
211
+ const slicesToPersist =
212
+ semanticSlices.length > 0
213
+ ? semanticSlices
214
+ : new CallGraphTracer(this.repo, projectRoot).traceAllSlices();
192
215
 
193
- if (semanticSlices.length > 0) {
194
- for (const slice of semanticSlices) {
195
- this.repo.upsertVerticalSlice(slice);
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.setMeta("last_sync", new Date().toISOString());
210
- this.repo.setMeta("files_scanned", String(filesScanned));
211
- this.repo.setMeta("files_updated", String(filesUpdated));
212
- this.repo.setMeta("symbols_indexed", String(symbolsIndexed));
213
- this.repo.setMeta("dependencies_indexed", String(dependenciesIndexed));
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
- targetDomainName = "core";
539
- targetDomainCfg = { root: ".", allowed_dependencies: [], forbidden_dependencies: [], archetypes: {} };
561
+ return false;
540
562
  }
541
563
 
542
564
  const domainId = this.repo.upsertDomain(targetDomainName, targetDomainCfg);