septum 0.1.6 → 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 +38 -37
- package/package.json +6 -5
- package/src/cli/commands/check.ts +34 -122
- package/src/cli/commands/hook.ts +68 -16
- 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 +7 -68
- 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/evaluator.ts +21 -25
- package/src/core/boundary/staged-auditor.ts +50 -0
- package/src/core/config/loader.ts +21 -24
- package/src/core/database/client.ts +8 -0
- package/src/core/database/repositories/dependency.repository.ts +80 -16
- package/src/core/database/repositories/domain.repository.ts +86 -33
- package/src/core/database/repositories/symbol.repository.ts +147 -65
- 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 +274 -126
- package/src/core/parser/extractors/frontend.ts +1 -0
- package/src/core/parser/extractors/go.ts +2 -2
- 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/extractors/php.ts +2 -2
- package/src/core/parser/extractors/python.ts +2 -2
- package/src/core/parser/extractors/typescript.ts +2 -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/parser/tree-sitter.ts +6 -1
- package/src/core/parser/wasm-assets.ts +17 -0
- package/src/core/parser/wasm-loader.ts +3 -0
- package/src/core/resolver/call-graph-tracer.ts +4 -3
- package/src/core/resolver/module-resolver.ts +1 -10
- package/src/core/resolver/path-resolver.ts +57 -4
- package/src/core/resolver/vertical-slice-tracer.ts +1 -0
- 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 +67 -38
- 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 +326 -529
- package/src/mcp/tools/boundary.ts +17 -0
- package/src/mcp/tools/check-boundary.ts +97 -43
- package/src/mcp/tools/domain.ts +40 -0
- package/src/mcp/tools/feature.ts +67 -0
- package/src/mcp/tools/get-domain-catalog.ts +15 -7
- 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 +28 -5
- package/src/mcp/tools/register-domain.ts +25 -9
- package/src/mcp/tools/register-feature.ts +40 -21
- 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 +21 -1
- package/src/types/wasm.d.ts +4 -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
|
@@ -21,50 +21,103 @@ export class DomainRepository {
|
|
|
21
21
|
constructor(private db: Database) {}
|
|
22
22
|
|
|
23
23
|
public upsertDomain(name: string, config: DomainConfig): number {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
return this.db.transaction(() => {
|
|
25
|
+
const normalizedName = name.trim();
|
|
26
|
+
let existing = this.getDomainByName(normalizedName);
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// MCP-13: If duplicates exist with different casing, consolidate into canonical primary row
|
|
29
|
+
if (existing) {
|
|
30
|
+
const allMatching = this.db
|
|
31
|
+
.query<DomainRecord, [string]>("SELECT * FROM domains WHERE LOWER(name) = LOWER(?) ORDER BY id ASC")
|
|
32
|
+
.all(normalizedName);
|
|
33
|
+
if (allMatching.length > 1) {
|
|
34
|
+
const primary = allMatching[0];
|
|
35
|
+
const fields = [
|
|
36
|
+
["allowed_dependencies", "allowed_deps_json"],
|
|
37
|
+
["forbidden_dependencies", "forbidden_deps_json"],
|
|
38
|
+
["archetypes", "archetypes_json"],
|
|
39
|
+
] as const;
|
|
40
|
+
for (const [field, column] of fields) {
|
|
41
|
+
if (config[field] !== undefined) continue;
|
|
42
|
+
const values = allMatching.map(row => {
|
|
43
|
+
const parsed = JSON.parse(row[column]);
|
|
44
|
+
return JSON.stringify(Array.isArray(parsed) ? [...parsed].sort() : Object.entries(parsed).sort());
|
|
45
|
+
});
|
|
46
|
+
if (new Set(values).size > 1) {
|
|
47
|
+
throw new Error(`Conflicting legacy domain '${normalizedName}': provide '${field}' explicitly before consolidating case variants.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (let i = 1; i < allMatching.length; i++) {
|
|
51
|
+
const duplicate = allMatching[i];
|
|
52
|
+
this.db.query("UPDATE files SET domain_id = ? WHERE domain_id = ?").run(primary.id, duplicate.id);
|
|
53
|
+
this.db.query("UPDATE vertical_slices SET domain_id = ? WHERE domain_id = ?").run(primary.id, duplicate.id);
|
|
54
|
+
this.db.query("DELETE FROM domains WHERE id = ?").run(duplicate.id);
|
|
55
|
+
}
|
|
56
|
+
existing = primary;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
31
59
|
|
|
32
|
-
|
|
33
|
-
|
|
60
|
+
const targetName = existing ? existing.name : normalizedName;
|
|
61
|
+
|
|
62
|
+
// MCP-12: Distinguish absent (undefined -> preserve) from explicit empty ([] -> clear)
|
|
63
|
+
let allowed: string[];
|
|
64
|
+
if (config.allowed_dependencies !== undefined) {
|
|
65
|
+
allowed = config.allowed_dependencies;
|
|
66
|
+
} else if (existing?.allowed_deps_json) {
|
|
34
67
|
try {
|
|
35
68
|
allowed = JSON.parse(existing.allowed_deps_json);
|
|
36
|
-
} catch {
|
|
69
|
+
} catch {
|
|
70
|
+
allowed = [];
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
allowed = [];
|
|
37
74
|
}
|
|
38
|
-
|
|
75
|
+
|
|
76
|
+
let forbidden: string[];
|
|
77
|
+
if (config.forbidden_dependencies !== undefined) {
|
|
78
|
+
forbidden = config.forbidden_dependencies;
|
|
79
|
+
} else if (existing?.forbidden_deps_json) {
|
|
39
80
|
try {
|
|
40
81
|
forbidden = JSON.parse(existing.forbidden_deps_json);
|
|
41
|
-
} catch {
|
|
82
|
+
} catch {
|
|
83
|
+
forbidden = [];
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
forbidden = [];
|
|
42
87
|
}
|
|
43
|
-
|
|
88
|
+
|
|
89
|
+
let archetypes: Record<string, string>;
|
|
90
|
+
if (config.archetypes !== undefined) {
|
|
91
|
+
archetypes = config.archetypes;
|
|
92
|
+
} else if (existing?.archetypes_json) {
|
|
44
93
|
try {
|
|
45
94
|
archetypes = JSON.parse(existing.archetypes_json);
|
|
46
|
-
} catch {
|
|
95
|
+
} catch {
|
|
96
|
+
archetypes = {};
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
archetypes = {};
|
|
47
100
|
}
|
|
48
|
-
}
|
|
49
101
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
102
|
+
const allowedJson = JSON.stringify(allowed);
|
|
103
|
+
const forbiddenJson = JSON.stringify(forbidden);
|
|
104
|
+
const archetypesJson = JSON.stringify(archetypes);
|
|
105
|
+
|
|
106
|
+
const query = `
|
|
107
|
+
INSERT INTO domains (name, root_path, allowed_deps_json, forbidden_deps_json, archetypes_json)
|
|
108
|
+
VALUES (?, ?, ?, ?, ?)
|
|
109
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
110
|
+
root_path = excluded.root_path,
|
|
111
|
+
allowed_deps_json = excluded.allowed_deps_json,
|
|
112
|
+
forbidden_deps_json = excluded.forbidden_deps_json,
|
|
113
|
+
archetypes_json = excluded.archetypes_json,
|
|
114
|
+
updated_at = CURRENT_TIMESTAMP
|
|
115
|
+
`;
|
|
116
|
+
this.db.query(query).run(targetName, config.root, allowedJson, forbiddenJson, archetypesJson);
|
|
117
|
+
|
|
118
|
+
const record = this.getDomainByName(targetName);
|
|
119
|
+
return record?.id ?? 0;
|
|
120
|
+
}).immediate();
|
|
68
121
|
}
|
|
69
122
|
|
|
70
123
|
public getDomainByName(name: string): DomainRecord | null {
|
|
@@ -268,7 +321,7 @@ export class DomainRepository {
|
|
|
268
321
|
files: catalogFiles,
|
|
269
322
|
agent_hint:
|
|
270
323
|
detailLevel === "compact"
|
|
271
|
-
? "Compact view (<400 tokens). Use
|
|
324
|
+
? "Compact view (<400 tokens). Use septum_symbol({ action: 'inspect', query: symbol }) for exact method signatures, or septum_domain({ action: 'catalog', domain, detail_level: 'full', file_filter: '<path>' }) for full member lists."
|
|
272
325
|
: undefined,
|
|
273
326
|
};
|
|
274
327
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
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,
|
|
10
|
+
ResolvedOutboundCall,
|
|
11
|
+
SymbolComplexitySmell,
|
|
8
12
|
SymbolKind,
|
|
9
13
|
SymbolRecord,
|
|
10
14
|
} from "../../../types/index.ts";
|
|
@@ -310,7 +314,7 @@ export class SymbolRepository {
|
|
|
310
314
|
|
|
311
315
|
public getSymbolDetails(
|
|
312
316
|
symbolQuery: string,
|
|
313
|
-
options: { domain?: string; includeDependencies?: boolean } = {}
|
|
317
|
+
options: { domain?: string; includeDependencies?: boolean; workspaceRoot?: string } = {}
|
|
314
318
|
): GetSymbolDetailsResponse | null {
|
|
315
319
|
const trimmed = symbolQuery.trim();
|
|
316
320
|
if (!trimmed) return null;
|
|
@@ -380,85 +384,161 @@ export class SymbolRepository {
|
|
|
380
384
|
|
|
381
385
|
// 2. Dependencies Injected (from signature type-hints and constructor)
|
|
382
386
|
const dependenciesInjected = new Set<string>();
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
+
}
|
|
397
409
|
}
|
|
398
410
|
}
|
|
399
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
|
+
}
|
|
400
426
|
}
|
|
401
|
-
};
|
|
402
427
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
+
}
|
|
447
|
+
}
|
|
416
448
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const end = Math.min(lines.length, row.line_end);
|
|
425
|
-
const rawSlice = lines.slice(start, end).join("\n");
|
|
426
|
-
const sanitizedSlice = stripCommentsAndStrings(rawSlice);
|
|
427
|
-
|
|
428
|
-
// Match Static invocations: ClassName::methodName(
|
|
429
|
-
const staticCalls = sanitizedSlice.matchAll(/([A-Z][a-zA-Z0-9_]+)::([a-zA-Z0-9_]+)\s*\(/g);
|
|
430
|
-
for (const m of staticCalls) {
|
|
431
|
-
if (m[1] !== "self" && m[1] !== "parent" && m[1] !== "static") {
|
|
432
|
-
outboundCalls.add(`${m[1]}::${m[2]}`);
|
|
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];
|
|
454
|
+
const inferredClass = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
455
|
+
outboundCalls.add(`${inferredClass}::${methodName}`);
|
|
433
456
|
}
|
|
434
|
-
}
|
|
435
457
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
+
}
|
|
443
468
|
}
|
|
469
|
+
} catch {
|
|
470
|
+
// Graceful fallback if file not physically accessible
|
|
471
|
+
}
|
|
444
472
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
+
});
|
|
453
523
|
}
|
|
524
|
+
} catch {
|
|
525
|
+
resolvedOutboundCalls.push({
|
|
526
|
+
symbol: call,
|
|
527
|
+
is_resolved: false,
|
|
528
|
+
});
|
|
454
529
|
}
|
|
455
530
|
}
|
|
456
|
-
} catch {
|
|
457
|
-
// Graceful fallback if file not physically accessible
|
|
458
531
|
}
|
|
459
532
|
|
|
460
|
-
const
|
|
461
|
-
|
|
533
|
+
const complexitySmell: SymbolComplexitySmell | undefined =
|
|
534
|
+
row.line_count > 300
|
|
535
|
+
? {
|
|
536
|
+
is_high_complexity: true,
|
|
537
|
+
threshold: 300,
|
|
538
|
+
total_lines: row.line_count,
|
|
539
|
+
warning: `Symbol '${row.name}' spans ${row.line_count} lines (exceeds 300-line threshold). Candidate for decomposition/refactoring.`,
|
|
540
|
+
}
|
|
541
|
+
: undefined;
|
|
462
542
|
|
|
463
543
|
return {
|
|
464
544
|
symbol: row.name,
|
|
@@ -474,6 +554,8 @@ export class SymbolRepository {
|
|
|
474
554
|
visibility: row.visibility,
|
|
475
555
|
dependencies_injected: Array.from(dependenciesInjected),
|
|
476
556
|
outbound_calls: Array.from(outboundCalls),
|
|
557
|
+
resolved_outbound_calls: resolvedOutboundCalls.length > 0 ? resolvedOutboundCalls : undefined,
|
|
558
|
+
complexity_smell: complexitySmell,
|
|
477
559
|
sibling_methods: siblingMethods.length > 0 ? siblingMethods : undefined,
|
|
478
560
|
connected_files: connectedFiles,
|
|
479
561
|
};
|
|
@@ -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 {
|