septum 0.1.7 → 0.1.8
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/README.md +36 -55
- package/package.json +3 -2
- package/src/cli/commands/check.ts +2 -0
- package/src/cli/commands/hook.ts +59 -12
- package/src/cli/commands/init.ts +440 -121
- package/src/cli/commands/sync.ts +27 -17
- package/src/cli/commands/topology.ts +89 -47
- package/src/cli/index.ts +5 -67
- package/src/cli/scaffold/harness-config.ts +697 -0
- package/src/cli/templates/hooks.ts +6 -6
- package/src/cli/templates/rules.ts +8 -8
- package/src/cli/templates/skill.ts +11 -18
- package/src/cli/ui/prompts.ts +122 -0
- package/src/core/boundary/staged-auditor.ts +5 -2
- package/src/core/database/client.ts +8 -0
- package/src/core/database/repositories/dependency.repository.ts +77 -13
- package/src/core/database/repositories/domain.repository.ts +86 -33
- package/src/core/database/repositories/symbol.repository.ts +128 -117
- package/src/core/database/repository.ts +17 -5
- package/src/core/database/schema.ts +1 -1
- package/src/core/discovery/topology-detector.ts +71 -19
- package/src/core/ingestion/pipeline.ts +228 -94
- package/src/core/parser/extractors/laravel-semantic.ts +9 -8
- package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
- package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
- package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
- package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
- package/src/core/resolver/call-graph-tracer.ts +4 -3
- package/src/core/resolver/path-resolver.ts +57 -4
- package/src/core/resources/ignore.ts +35 -0
- package/src/core/resources/limits.ts +14 -0
- package/src/core/resources/source-files.ts +62 -0
- package/src/core/resources/work-queue.ts +74 -0
- package/src/core/session/session-manager.ts +19 -6
- package/src/core/telemetry/telemetry.ts +12 -1
- package/src/core/topology/environment-detector.ts +21 -11
- package/src/mcp/schemas.ts +381 -8
- package/src/mcp/server.ts +321 -535
- package/src/mcp/tools/boundary.ts +17 -0
- package/src/mcp/tools/check-boundary.ts +83 -11
- package/src/mcp/tools/domain.ts +40 -0
- package/src/mcp/tools/feature.ts +67 -0
- package/src/mcp/tools/get-domain-catalog.ts +9 -6
- package/src/mcp/tools/get-environment-topology.ts +2 -2
- package/src/mcp/tools/get-feature-context.ts +9 -12
- package/src/mcp/tools/get-symbol-impact.ts +5 -2
- package/src/mcp/tools/get-symbol.ts +2 -1
- package/src/mcp/tools/locate-symbol.ts +3 -3
- package/src/mcp/tools/register-domain.ts +25 -9
- package/src/mcp/tools/register-feature.ts +34 -17
- package/src/mcp/tools/slice.ts +15 -0
- package/src/mcp/tools/symbol.ts +82 -0
- package/src/mcp/workspace-manager.ts +159 -0
- package/src/mcp/workspace-watcher.ts +202 -0
- package/src/types/index.ts +1 -0
- package/src/cli/commands/ingest.ts +0 -29
- package/src/cli/commands/locate.ts +0 -61
- package/src/cli/commands/query.ts +0 -104
- package/src/cli/commands/slice.ts +0 -50
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import type { Database } from "bun:sqlite";
|
|
3
4
|
import type {
|
|
4
5
|
DomainRecord,
|
|
5
6
|
ExtractedSymbol,
|
|
6
7
|
FileRecord,
|
|
7
8
|
GetSymbolDetailsResponse,
|
|
9
|
+
FileConnectionsSummary,
|
|
8
10
|
ResolvedOutboundCall,
|
|
9
11
|
SymbolComplexitySmell,
|
|
10
12
|
SymbolKind,
|
|
@@ -312,7 +314,7 @@ export class SymbolRepository {
|
|
|
312
314
|
|
|
313
315
|
public getSymbolDetails(
|
|
314
316
|
symbolQuery: string,
|
|
315
|
-
options: { domain?: string; includeDependencies?: boolean } = {}
|
|
317
|
+
options: { domain?: string; includeDependencies?: boolean; workspaceRoot?: string } = {}
|
|
316
318
|
): GetSymbolDetailsResponse | null {
|
|
317
319
|
const trimmed = symbolQuery.trim();
|
|
318
320
|
if (!trimmed) return null;
|
|
@@ -382,140 +384,149 @@ export class SymbolRepository {
|
|
|
382
384
|
|
|
383
385
|
// 2. Dependencies Injected (from signature type-hints and constructor)
|
|
384
386
|
const dependenciesInjected = new Set<string>();
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
387
|
+
const outboundCalls = new Set<string>();
|
|
388
|
+
const resolvedOutboundCalls: ResolvedOutboundCall[] = [];
|
|
389
|
+
let connectedFiles: FileConnectionsSummary = { imports_from: [], imported_by_count: 0 };
|
|
390
|
+
|
|
391
|
+
const shouldIncludeDeps = options.includeDependencies !== false;
|
|
392
|
+
|
|
393
|
+
if (shouldIncludeDeps) {
|
|
394
|
+
const extractTypeHints = (sig: string) => {
|
|
395
|
+
const paramMatch = sig.match(/\(([^)]*)\)/);
|
|
396
|
+
if (paramMatch && paramMatch[1]) {
|
|
397
|
+
const params = paramMatch[1].split(",");
|
|
398
|
+
const primitiveTypes = new Set([
|
|
399
|
+
"int", "string", "bool", "boolean", "float", "array", "iterable",
|
|
400
|
+
"callable", "void", "mixed", "object", "null", "never", "any", "number", "unknown"
|
|
401
|
+
]);
|
|
402
|
+
for (const p of params) {
|
|
403
|
+
const parts = p.trim().replace(/^\?/, "").split(/\s+/);
|
|
404
|
+
if (parts.length >= 2) {
|
|
405
|
+
const rawType = parts[0].replace(/\[\]$/, "").replace(/^[\\$]/, "");
|
|
406
|
+
if (rawType && !primitiveTypes.has(rawType.toLowerCase()) && !rawType.startsWith("$")) {
|
|
407
|
+
dependenciesInjected.add(rawType);
|
|
408
|
+
}
|
|
399
409
|
}
|
|
400
410
|
}
|
|
401
411
|
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
extractTypeHints(row.signature);
|
|
415
|
+
|
|
416
|
+
// Also check constructor injection if this is a method
|
|
417
|
+
if (containerName) {
|
|
418
|
+
const ctor = this.db
|
|
419
|
+
.query<{ signature: string }, [number, string, string]>(
|
|
420
|
+
"SELECT signature FROM symbols WHERE file_id = ? AND (name = ? OR name LIKE ?) LIMIT 1"
|
|
421
|
+
)
|
|
422
|
+
.get(row.file_id, `${containerName}::__construct`, `%::__construct`);
|
|
423
|
+
if (ctor) {
|
|
424
|
+
extractTypeHints(ctor.signature);
|
|
425
|
+
}
|
|
402
426
|
}
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
extractTypeHints(row.signature);
|
|
406
|
-
|
|
407
|
-
// Also check constructor injection if this is a method
|
|
408
|
-
if (containerName) {
|
|
409
|
-
const ctor = this.db
|
|
410
|
-
.query<{ signature: string }, [number, string, string]>(
|
|
411
|
-
"SELECT signature FROM symbols WHERE file_id = ? AND (name = ? OR name LIKE ?) LIMIT 1"
|
|
412
|
-
)
|
|
413
|
-
.get(row.file_id, `${containerName}::__construct`, `%::__construct`);
|
|
414
|
-
if (ctor) {
|
|
415
|
-
extractTypeHints(ctor.signature);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
427
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
428
|
+
// 3. Outbound Calls (inspected from physical source file lines if accessible)
|
|
429
|
+
try {
|
|
430
|
+
const fullPath = options.workspaceRoot
|
|
431
|
+
? path.resolve(options.workspaceRoot, row.file_path)
|
|
432
|
+
: row.file_path;
|
|
433
|
+
if (fs.existsSync(fullPath)) {
|
|
434
|
+
const fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
435
|
+
const lines = fileContent.split(/\r?\n/);
|
|
436
|
+
const start = Math.max(0, row.line_start - 1);
|
|
437
|
+
const end = Math.min(lines.length, row.line_end);
|
|
438
|
+
const rawSlice = lines.slice(start, end).join("\n");
|
|
439
|
+
const sanitizedSlice = stripCommentsAndStrings(rawSlice);
|
|
440
|
+
|
|
441
|
+
// Match Static invocations: ClassName::methodName(
|
|
442
|
+
const staticCalls = sanitizedSlice.matchAll(/([A-Z][a-zA-Z0-9_]+)::([a-zA-Z0-9_]+)\s*\(/g);
|
|
443
|
+
for (const m of staticCalls) {
|
|
444
|
+
if (m[1] !== "self" && m[1] !== "parent" && m[1] !== "static") {
|
|
445
|
+
outboundCalls.add(`${m[1]}::${m[2]}`);
|
|
446
|
+
}
|
|
435
447
|
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Match PHP property calls: $this->property->method(
|
|
439
|
-
const propCalls = sanitizedSlice.matchAll(/\$this->([a-zA-Z0-9_]+)->([a-zA-Z0-9_]+)\s*\(/g);
|
|
440
|
-
for (const m of propCalls) {
|
|
441
|
-
const propName = m[1];
|
|
442
|
-
const methodName = m[2];
|
|
443
|
-
const inferredClass = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
444
|
-
outboundCalls.add(`${inferredClass}::${methodName}`);
|
|
445
|
-
}
|
|
446
448
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (propName !== "db") {
|
|
449
|
+
// Match PHP property calls: $this->property->method(
|
|
450
|
+
const propCalls = sanitizedSlice.matchAll(/\$this->([a-zA-Z0-9_]+)->([a-zA-Z0-9_]+)\s*\(/g);
|
|
451
|
+
for (const m of propCalls) {
|
|
452
|
+
const propName = m[1];
|
|
453
|
+
const methodName = m[2];
|
|
453
454
|
const inferredClass = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
454
455
|
outboundCalls.add(`${inferredClass}::${methodName}`);
|
|
455
456
|
}
|
|
457
|
+
|
|
458
|
+
// Match TS/JS property calls: this.property.method(
|
|
459
|
+
const tsPropCalls = sanitizedSlice.matchAll(/\bthis\.([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\s*\(/g);
|
|
460
|
+
for (const m of tsPropCalls) {
|
|
461
|
+
const propName = m[1];
|
|
462
|
+
const methodName = m[2];
|
|
463
|
+
if (propName !== "db") {
|
|
464
|
+
const inferredClass = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
465
|
+
outboundCalls.add(`${inferredClass}::${methodName}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
456
468
|
}
|
|
469
|
+
} catch {
|
|
470
|
+
// Graceful fallback if file not physically accessible
|
|
457
471
|
}
|
|
458
|
-
} catch {
|
|
459
|
-
// Graceful fallback if file not physically accessible
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const depRepo = new DependencyRepository(this.db);
|
|
463
|
-
const connectedFiles = depRepo.getFileConnections(row.file_path);
|
|
464
472
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
473
|
+
const depRepo = new DependencyRepository(this.db);
|
|
474
|
+
connectedFiles = depRepo.getFileConnections(row.file_path);
|
|
475
|
+
|
|
476
|
+
// 4. Resolve Outbound Calls to target symbol line ranges
|
|
477
|
+
const lookupStmt = this.db.query<
|
|
478
|
+
{
|
|
479
|
+
name: string;
|
|
480
|
+
line_start: number;
|
|
481
|
+
line_end: number;
|
|
482
|
+
line_count: number;
|
|
483
|
+
file_path: string;
|
|
484
|
+
domain_name: string;
|
|
485
|
+
},
|
|
486
|
+
[string, string, string, string]
|
|
487
|
+
>(`
|
|
488
|
+
SELECT
|
|
489
|
+
s.name,
|
|
490
|
+
s.line_start,
|
|
491
|
+
s.line_end,
|
|
492
|
+
s.line_count,
|
|
493
|
+
f.path AS file_path,
|
|
494
|
+
d.name AS domain_name
|
|
495
|
+
FROM symbols s
|
|
496
|
+
JOIN files f ON s.file_id = f.id
|
|
497
|
+
JOIN domains d ON f.domain_id = d.id
|
|
498
|
+
WHERE (s.name = ? OR s.name LIKE ? OR s.name LIKE ?)
|
|
499
|
+
ORDER BY CASE WHEN s.name = ? THEN 1 ELSE 2 END, s.line_start ASC
|
|
500
|
+
LIMIT 1
|
|
501
|
+
`);
|
|
502
|
+
|
|
503
|
+
for (const call of outboundCalls) {
|
|
504
|
+
try {
|
|
505
|
+
const target = lookupStmt.get(call, `%::${call}`, `${call}::%`, call);
|
|
506
|
+
if (target) {
|
|
507
|
+
resolvedOutboundCalls.push({
|
|
508
|
+
symbol: call,
|
|
509
|
+
target_file: target.file_path,
|
|
510
|
+
target_range: {
|
|
511
|
+
start_line: target.line_start,
|
|
512
|
+
end_line: target.line_end,
|
|
513
|
+
total_lines: target.line_count,
|
|
514
|
+
},
|
|
515
|
+
domain: target.domain_name,
|
|
516
|
+
is_resolved: true,
|
|
517
|
+
});
|
|
518
|
+
} else {
|
|
519
|
+
resolvedOutboundCalls.push({
|
|
520
|
+
symbol: call,
|
|
521
|
+
is_resolved: false,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
509
525
|
resolvedOutboundCalls.push({
|
|
510
526
|
symbol: call,
|
|
511
527
|
is_resolved: false,
|
|
512
528
|
});
|
|
513
529
|
}
|
|
514
|
-
} catch {
|
|
515
|
-
resolvedOutboundCalls.push({
|
|
516
|
-
symbol: call,
|
|
517
|
-
is_resolved: false,
|
|
518
|
-
});
|
|
519
530
|
}
|
|
520
531
|
}
|
|
521
532
|
|
|
@@ -56,8 +56,17 @@ export class SeptumRepository {
|
|
|
56
56
|
this.environment = new EnvironmentRepository(db);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
public runInTransaction<T>(fn: () => T): T {
|
|
60
|
-
|
|
59
|
+
public runInTransaction<T>(fn: () => T, immediate = false): T {
|
|
60
|
+
const transaction = this.db.transaction(fn);
|
|
61
|
+
return immediate ? transaction.immediate() : transaction();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public checkpoint(): void {
|
|
65
|
+
try {
|
|
66
|
+
this.db.run("PRAGMA wal_checkpoint(PASSIVE);");
|
|
67
|
+
} catch {
|
|
68
|
+
// Ignore if locked or busy
|
|
69
|
+
}
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
// --- Domain Methods ---
|
|
@@ -247,13 +256,16 @@ export class SeptumRepository {
|
|
|
247
256
|
|
|
248
257
|
public getSymbolDetails(
|
|
249
258
|
symbolQuery: string,
|
|
250
|
-
options: { domain?: string; includeDependencies?: boolean } = {}
|
|
259
|
+
options: { domain?: string; includeDependencies?: boolean; workspaceRoot?: string } = {}
|
|
251
260
|
): GetSymbolDetailsResponse | null {
|
|
252
261
|
return this.symbols.getSymbolDetails(symbolQuery, options);
|
|
253
262
|
}
|
|
254
263
|
|
|
255
|
-
public getSymbolImpact(
|
|
256
|
-
|
|
264
|
+
public getSymbolImpact(
|
|
265
|
+
symbolName: string,
|
|
266
|
+
options: { domain?: string; workspaceRoot?: string } = {}
|
|
267
|
+
): GetSymbolImpactResponse {
|
|
268
|
+
return this.dependencies.getSymbolImpact(symbolName, options);
|
|
257
269
|
}
|
|
258
270
|
|
|
259
271
|
public getFileInboundImpact(filePath: string): {
|
|
@@ -5,7 +5,7 @@ export const CURRENT_SCHEMA_VERSION = 4;
|
|
|
5
5
|
export const TABLE_STATEMENTS = [
|
|
6
6
|
`CREATE TABLE IF NOT EXISTS domains (
|
|
7
7
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8
|
-
name TEXT NOT NULL UNIQUE,
|
|
8
|
+
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
9
9
|
root_path TEXT NOT NULL,
|
|
10
10
|
allowed_deps_json TEXT NOT NULL DEFAULT '[]',
|
|
11
11
|
forbidden_deps_json TEXT NOT NULL DEFAULT '[]',
|
|
@@ -126,29 +126,63 @@ export class TopologyDetector {
|
|
|
126
126
|
if (isMonorepo) {
|
|
127
127
|
architectureStyle = "monorepo-workspaces";
|
|
128
128
|
const workspaceDirs = ["apps", "packages", "modules"];
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
interface WorkspaceCandidate {
|
|
130
|
+
parent: string;
|
|
131
|
+
sub: string;
|
|
132
|
+
relRoot: string;
|
|
133
|
+
}
|
|
134
|
+
const candidates: WorkspaceCandidate[] = [];
|
|
131
135
|
|
|
132
136
|
for (const parent of workspaceDirs) {
|
|
133
137
|
const fullParent = path.join(projectRoot, parent);
|
|
134
138
|
if (fs.existsSync(fullParent) && fs.statSync(fullParent).isDirectory()) {
|
|
135
139
|
const subs = this.getSubdirectories(fullParent);
|
|
136
140
|
for (const sub of subs) {
|
|
137
|
-
|
|
138
|
-
const domainName = sub.toLowerCase();
|
|
139
|
-
if (parent === "apps") appDomains.push(domainName);
|
|
140
|
-
else packageDomains.push(domainName);
|
|
141
|
-
|
|
142
|
-
domains[domainName] = {
|
|
143
|
-
root: relRoot,
|
|
144
|
-
description: `Monorepo ${parent} package: ${sub}`,
|
|
145
|
-
allowed_dependencies: parent === "apps" ? packageDomains : [],
|
|
146
|
-
forbidden_dependencies: parent !== "apps" ? appDomains : [],
|
|
147
|
-
archetypes: this.inferDefaultArchetypes(path.join(projectRoot, relRoot)),
|
|
148
|
-
};
|
|
141
|
+
candidates.push({ parent, sub, relRoot: path.join(parent, sub) });
|
|
149
142
|
}
|
|
150
143
|
}
|
|
151
144
|
}
|
|
145
|
+
|
|
146
|
+
// Detect collisions across workspace subdirectories
|
|
147
|
+
const nameCounts = new Map<string, number>();
|
|
148
|
+
for (const c of candidates) {
|
|
149
|
+
const lower = c.sub.toLowerCase();
|
|
150
|
+
nameCounts.set(lower, (nameCounts.get(lower) || 0) + 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const appDomains: string[] = [];
|
|
154
|
+
const packageDomains: string[] = [];
|
|
155
|
+
const items: Array<{ domainName: string; parent: string; sub: string; relRoot: string }> = [];
|
|
156
|
+
|
|
157
|
+
for (const c of candidates) {
|
|
158
|
+
const lower = c.sub.toLowerCase();
|
|
159
|
+
let domainName = (nameCounts.get(lower) || 0) > 1
|
|
160
|
+
? `${c.parent}-${lower}`
|
|
161
|
+
: lower;
|
|
162
|
+
|
|
163
|
+
let uniqueName = domainName;
|
|
164
|
+
let counter = 2;
|
|
165
|
+
while (items.some((it) => it.domainName === uniqueName)) {
|
|
166
|
+
uniqueName = `${domainName}-${counter++}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (c.parent === "apps") {
|
|
170
|
+
appDomains.push(uniqueName);
|
|
171
|
+
} else {
|
|
172
|
+
packageDomains.push(uniqueName);
|
|
173
|
+
}
|
|
174
|
+
items.push({ domainName: uniqueName, parent: c.parent, sub: c.sub, relRoot: c.relRoot });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const item of items) {
|
|
178
|
+
domains[item.domainName] = {
|
|
179
|
+
root: item.relRoot,
|
|
180
|
+
description: `Monorepo ${item.parent} package: ${item.sub}`,
|
|
181
|
+
allowed_dependencies: item.parent === "apps" ? packageDomains : [],
|
|
182
|
+
forbidden_dependencies: item.parent !== "apps" ? appDomains : [],
|
|
183
|
+
archetypes: this.inferDefaultArchetypes(path.join(projectRoot, item.relRoot)),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
152
186
|
}
|
|
153
187
|
|
|
154
188
|
// 2. Modular DDD (app/Domain, src/modules, src/domains, etc.)
|
|
@@ -308,15 +342,21 @@ export class TopologyDetector {
|
|
|
308
342
|
|
|
309
343
|
/**
|
|
310
344
|
* Discovers project topology and persists domains and macro metadata directly into SQLite SSOT.
|
|
345
|
+
* If a domain is already registered, its existing root and policy are preserved on repeat init,
|
|
346
|
+
* while new domains discovered from the codebase are added.
|
|
311
347
|
*/
|
|
312
348
|
public static discoverAndPersist(
|
|
313
349
|
repo: SeptumRepository,
|
|
314
|
-
projectRoot: string = process.cwd()
|
|
350
|
+
projectRoot: string = process.cwd(),
|
|
351
|
+
forceResetPolicies: boolean = false
|
|
315
352
|
): Record<string, DomainConfig> {
|
|
316
353
|
const topology = this.detect(projectRoot);
|
|
317
354
|
|
|
318
355
|
for (const [name, config] of Object.entries(topology.domains)) {
|
|
319
|
-
repo.
|
|
356
|
+
const existing = repo.domains.getDomainByName(name);
|
|
357
|
+
if (!existing || forceResetPolicies) {
|
|
358
|
+
repo.upsertDomain(name, config);
|
|
359
|
+
}
|
|
320
360
|
}
|
|
321
361
|
|
|
322
362
|
// Persist Macro Metadata SSOT
|
|
@@ -324,18 +364,30 @@ export class TopologyDetector {
|
|
|
324
364
|
repo.setMeta("architecture_style", topology.architectureStyle);
|
|
325
365
|
repo.setMeta("framework", topology.framework);
|
|
326
366
|
repo.setMeta("language", topology.language);
|
|
367
|
+
|
|
368
|
+
const allRecords = repo.domains.getAllDomains();
|
|
327
369
|
repo.setMeta(
|
|
328
370
|
"macro_summary",
|
|
329
371
|
JSON.stringify({
|
|
330
372
|
projectType: topology.projectType,
|
|
331
373
|
framework: topology.framework,
|
|
332
374
|
architectureStyle: topology.architectureStyle,
|
|
333
|
-
domainsCount:
|
|
334
|
-
domains:
|
|
375
|
+
domainsCount: allRecords.length,
|
|
376
|
+
domains: allRecords.map((r) => r.name),
|
|
335
377
|
})
|
|
336
378
|
);
|
|
337
379
|
|
|
338
|
-
|
|
380
|
+
const result: Record<string, DomainConfig> = {};
|
|
381
|
+
for (const record of allRecords) {
|
|
382
|
+
result[record.name] = {
|
|
383
|
+
root: record.root_path,
|
|
384
|
+
allowed_dependencies: JSON.parse(record.allowed_deps_json || "[]"),
|
|
385
|
+
forbidden_dependencies: JSON.parse(record.forbidden_deps_json || "[]"),
|
|
386
|
+
archetypes: JSON.parse(record.archetypes_json || "{}"),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return result;
|
|
339
391
|
}
|
|
340
392
|
|
|
341
393
|
private static detectMonorepo(projectRoot: string): boolean {
|