septum 0.1.2 β 0.1.4
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 +7 -2
- package/package.json +1 -1
- package/src/cli/commands/slice.ts +4 -46
- package/src/cli/commands/topology.ts +119 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/templates/skill.ts +7 -4
- package/src/core/config/loader.ts +21 -8
- package/src/core/database/repositories/dependency.repository.ts +70 -0
- package/src/core/database/repositories/environment.repository.ts +207 -0
- package/src/core/database/repositories/file.repository.ts +7 -0
- package/src/core/database/repositories/symbol.repository.ts +28 -9
- package/src/core/database/repository.ts +19 -1
- package/src/core/database/schema.ts +153 -120
- package/src/core/discovery/topology-detector.ts +71 -3
- package/src/core/ingestion/pipeline.ts +98 -1
- package/src/core/parser/behavioral-analyzer.ts +119 -0
- package/src/core/parser/boundary-tracker.ts +160 -0
- package/src/core/parser/extractors/base.ts +2 -0
- package/src/core/parser/extractors/blade.ts +158 -0
- package/src/core/parser/extractors/go.ts +5 -1
- package/src/core/parser/extractors/laravel-semantic.ts +506 -31
- package/src/core/parser/extractors/php.ts +5 -1
- package/src/core/parser/extractors/python.ts +3 -1
- package/src/core/parser/extractors/registry.ts +21 -0
- package/src/core/parser/extractors/typescript.ts +5 -1
- package/src/core/parser/schema/providers/laravel-migration-provider.ts +215 -0
- package/src/core/parser/schema/providers/prisma-schema-provider.ts +89 -0
- package/src/core/parser/schema/schema-provider-registry.ts +43 -0
- package/src/core/parser/schema/schema-provider.interface.ts +33 -0
- package/src/core/parser/tree-sitter.ts +2 -0
- package/src/core/resolver/path-resolver.ts +147 -0
- package/src/core/resolver/vertical-slice-tracer.ts +56 -8
- package/src/core/topology/environment-detector.ts +860 -0
- package/src/core/topology/types.ts +83 -0
- package/src/index.ts +3 -0
- package/src/mcp/schemas.ts +264 -46
- package/src/mcp/server.ts +130 -26
- package/src/mcp/tools/check-boundary.ts +58 -8
- package/src/mcp/tools/get-domain-catalog.ts +31 -1
- package/src/mcp/tools/get-environment-topology.ts +141 -0
- package/src/mcp/tools/get-symbol-hotspots.ts +13 -3
- package/src/mcp/tools/locate-symbol.ts +119 -3
- package/src/mcp/tools/register-domain.ts +1 -1
- package/src/mcp/tools/trace-vertical-slice.ts +11 -65
- package/src/types/index.ts +20 -1
- package/src/version.ts +3 -3
package/src/mcp/server.ts
CHANGED
|
@@ -41,10 +41,12 @@ import {
|
|
|
41
41
|
import { handleGetSymbol } from "./tools/get-symbol.ts";
|
|
42
42
|
import { handleGetSymbolImpact } from "./tools/get-symbol-impact.ts";
|
|
43
43
|
import { handleRegisterDomain } from "./tools/register-domain.ts";
|
|
44
|
+
import { handleGetEnvironmentTopology } from "./tools/get-environment-topology.ts";
|
|
44
45
|
import type { GetSymbolArgs, GetSymbolImpactArgs } from "../types/index.ts";
|
|
45
46
|
import {
|
|
46
47
|
CheckBoundarySchema,
|
|
47
48
|
GetDomainCatalogSchema,
|
|
49
|
+
GetEnvironmentTopologySchema,
|
|
48
50
|
GetFeatureContextSchema,
|
|
49
51
|
GetSymbolHotspotsSchema,
|
|
50
52
|
GetSymbolImpactSchema,
|
|
@@ -54,12 +56,51 @@ import {
|
|
|
54
56
|
TraceVerticalSliceSchema,
|
|
55
57
|
} from "./schemas.ts";
|
|
56
58
|
import { SEPTUM_VERSION } from "../version.ts";
|
|
59
|
+
import { resolveWorkspaceRoot } from "../core/resolver/path-resolver.ts";
|
|
60
|
+
import type { ValidatedSeptumConfig } from "../core/config/schema.ts";
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
interface WorkspaceContext {
|
|
63
|
+
root: string;
|
|
64
|
+
config: ValidatedSeptumConfig;
|
|
65
|
+
db: SeptumDatabase;
|
|
66
|
+
repo: SeptumRepository;
|
|
67
|
+
evaluator: BoundaryEvaluator;
|
|
68
|
+
pipeline: IngestionPipeline;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const workspaceContextCache = new Map<string, WorkspaceContext>();
|
|
72
|
+
|
|
73
|
+
function extractPathHint(args: Record<string, any> | undefined): string | undefined {
|
|
74
|
+
if (!args) return undefined;
|
|
75
|
+
if (typeof args.workspace_path === "string" && args.workspace_path) return args.workspace_path;
|
|
76
|
+
if (typeof args.target_path === "string" && args.target_path) return args.target_path;
|
|
77
|
+
if (typeof args.project_root === "string" && args.project_root) return args.project_root;
|
|
78
|
+
if (typeof args.file_path === "string" && args.file_path) return args.file_path;
|
|
79
|
+
if (Array.isArray(args.file_paths) && args.file_paths[0]) return args.file_paths[0];
|
|
80
|
+
if (typeof args.root === "string" && args.root) return args.root;
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getWorkspaceContext(pathHint?: string): WorkspaceContext {
|
|
85
|
+
const root = resolveWorkspaceRoot(pathHint);
|
|
86
|
+
let ctx = workspaceContextCache.get(root);
|
|
87
|
+
if (ctx) {
|
|
88
|
+
return ctx;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const config = ConfigLoader.load(undefined, root);
|
|
60
92
|
const db = new SeptumDatabase(config.settings.db_path);
|
|
61
93
|
const repo = new SeptumRepository(db.raw);
|
|
62
94
|
const evaluator = new BoundaryEvaluator(repo);
|
|
95
|
+
const pipeline = new IngestionPipeline(repo, root);
|
|
96
|
+
|
|
97
|
+
ctx = { root, config, db, repo, evaluator, pipeline };
|
|
98
|
+
workspaceContextCache.set(root, ctx);
|
|
99
|
+
return ctx;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function runMCPServer(): Promise<void> {
|
|
103
|
+
const initialCtx = getWorkspaceContext();
|
|
63
104
|
|
|
64
105
|
const server = new Server(
|
|
65
106
|
{
|
|
@@ -141,7 +182,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
141
182
|
{
|
|
142
183
|
name: "septum_get_symbol_hotspots",
|
|
143
184
|
description:
|
|
144
|
-
"Discovers oversized functions, methods, and classes exceeding a specified line count threshold (
|
|
185
|
+
"Discovers oversized functions, methods, and classes exceeding a specified line count threshold or cyclomatic control flow nesting depth (> 4). Essential for Single Responsibility Principle (SRP) enforcement, finding God Functions / bug magnets, and isolating refactoring targets deterministically without reading raw files.",
|
|
145
186
|
inputSchema: {
|
|
146
187
|
type: "object",
|
|
147
188
|
properties: {
|
|
@@ -149,6 +190,10 @@ export async function runMCPServer(): Promise<void> {
|
|
|
149
190
|
type: "number",
|
|
150
191
|
description: "Minimum physical lines of code (LoC) threshold (default: 30).",
|
|
151
192
|
},
|
|
193
|
+
min_nesting: {
|
|
194
|
+
type: "number",
|
|
195
|
+
description: "Minimum control-flow nesting depth (e.g. nested if/for/while/try > 4).",
|
|
196
|
+
},
|
|
152
197
|
kind: {
|
|
153
198
|
type: "string",
|
|
154
199
|
description: "Filter by symbol kind ('function', 'method', 'class', 'struct', 'interface').",
|
|
@@ -161,6 +206,11 @@ export async function runMCPServer(): Promise<void> {
|
|
|
161
206
|
type: "number",
|
|
162
207
|
description: "Maximum results to return (default: 30).",
|
|
163
208
|
},
|
|
209
|
+
sort_by: {
|
|
210
|
+
type: "string",
|
|
211
|
+
enum: ["lines", "nesting", "risk_score"],
|
|
212
|
+
description: "Sorting criteria: 'risk_score' (combines lines & nesting depth), 'nesting', or 'lines'. Default: 'risk_score'.",
|
|
213
|
+
},
|
|
164
214
|
},
|
|
165
215
|
},
|
|
166
216
|
},
|
|
@@ -290,6 +340,41 @@ export async function runMCPServer(): Promise<void> {
|
|
|
290
340
|
required: ["name", "root"],
|
|
291
341
|
},
|
|
292
342
|
},
|
|
343
|
+
{
|
|
344
|
+
name: "septum_get_environment_topology",
|
|
345
|
+
description:
|
|
346
|
+
"Self-Aware Production Topology Engine: Maps and verifies the 6-layer environment topology (Edge/WAF, Gateway/Web Server, Host Platform, Runtime, Storage/Cache, Telemetry). Identifies cross-layer constraints (e.g. Cloudflare + LiteSpeed + Laravel throttle), surfaces pending architectural inquiries if layers are ambiguous, and allows recording verified environment answers to SQLite SSOT.",
|
|
347
|
+
inputSchema: {
|
|
348
|
+
type: "object",
|
|
349
|
+
properties: {
|
|
350
|
+
resolve: {
|
|
351
|
+
type: "object",
|
|
352
|
+
properties: {
|
|
353
|
+
layer: {
|
|
354
|
+
type: "string",
|
|
355
|
+
enum: ["edge", "gateway", "host", "runtime", "storage", "telemetry"],
|
|
356
|
+
description: "Target layer to manually resolve/verify",
|
|
357
|
+
},
|
|
358
|
+
platform: {
|
|
359
|
+
type: "string",
|
|
360
|
+
description: "Confirmed platform name (e.g. 'Hostinger Shared Hosting', 'Cloudflare WAF', 'LiteSpeed')",
|
|
361
|
+
},
|
|
362
|
+
constraints: {
|
|
363
|
+
type: "array",
|
|
364
|
+
items: { type: "string" },
|
|
365
|
+
description: "Optional list of architectural constraints for this layer",
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
required: ["layer", "platform"],
|
|
369
|
+
description: "Optional argument to record and verify a specific layer into SQLite SSOT",
|
|
370
|
+
},
|
|
371
|
+
refresh: {
|
|
372
|
+
type: "boolean",
|
|
373
|
+
description: "Force re-detection from codebase files, ignoring cached status",
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
293
378
|
],
|
|
294
379
|
};
|
|
295
380
|
});
|
|
@@ -297,7 +382,10 @@ export async function runMCPServer(): Promise<void> {
|
|
|
297
382
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
298
383
|
const { name, arguments: args } = request.params;
|
|
299
384
|
const startTime = performance.now();
|
|
300
|
-
const
|
|
385
|
+
const pathHint = extractPathHint(args as Record<string, any>);
|
|
386
|
+
const ctx = getWorkspaceContext(pathHint);
|
|
387
|
+
|
|
388
|
+
const activeSession = SessionManager.getActiveSession(ctx.root);
|
|
301
389
|
const sessionPayload = activeSession
|
|
302
390
|
? { feature_key: activeSession.feature_key, domain: activeSession.domain }
|
|
303
391
|
: null;
|
|
@@ -316,7 +404,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
316
404
|
],
|
|
317
405
|
};
|
|
318
406
|
}
|
|
319
|
-
return handleGetDomainCatalog(repo, config, parsed.data);
|
|
407
|
+
return handleGetDomainCatalog(ctx.repo, ctx.config, parsed.data, ctx.root);
|
|
320
408
|
} else if (name === "septum_get_feature_context") {
|
|
321
409
|
const parsed = GetFeatureContextSchema.safeParse(args ?? {});
|
|
322
410
|
if (!parsed.success) {
|
|
@@ -330,7 +418,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
330
418
|
],
|
|
331
419
|
};
|
|
332
420
|
}
|
|
333
|
-
return handleGetFeatureContext(repo, config, parsed.data);
|
|
421
|
+
return handleGetFeatureContext(ctx.repo, ctx.config, parsed.data);
|
|
334
422
|
} else if (name === "septum_locate_symbol") {
|
|
335
423
|
const parsed = LocateSymbolSchema.safeParse(args ?? {});
|
|
336
424
|
if (!parsed.success) {
|
|
@@ -344,7 +432,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
344
432
|
],
|
|
345
433
|
};
|
|
346
434
|
}
|
|
347
|
-
return handleLocateSymbol(repo, config, parsed.data);
|
|
435
|
+
return await handleLocateSymbol(ctx.repo, ctx.config, parsed.data, ctx.pipeline, ctx.root);
|
|
348
436
|
} else if (name === "septum_get_symbol") {
|
|
349
437
|
const parsed = GetSymbolSchema.safeParse(args ?? {});
|
|
350
438
|
if (!parsed.success) {
|
|
@@ -358,7 +446,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
358
446
|
],
|
|
359
447
|
};
|
|
360
448
|
}
|
|
361
|
-
return handleGetSymbol(repo, config, parsed.data);
|
|
449
|
+
return handleGetSymbol(ctx.repo, ctx.config, parsed.data);
|
|
362
450
|
} else if (name === "septum_get_symbol_impact") {
|
|
363
451
|
const parsed = GetSymbolImpactSchema.safeParse(args ?? {});
|
|
364
452
|
if (!parsed.success) {
|
|
@@ -372,7 +460,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
372
460
|
],
|
|
373
461
|
};
|
|
374
462
|
}
|
|
375
|
-
return handleGetSymbolImpact(repo, config, parsed.data);
|
|
463
|
+
return handleGetSymbolImpact(ctx.repo, ctx.config, parsed.data);
|
|
376
464
|
} else if (name === "septum_trace_vertical_slice") {
|
|
377
465
|
const parsed = TraceVerticalSliceSchema.safeParse(args ?? {});
|
|
378
466
|
if (!parsed.success) {
|
|
@@ -386,7 +474,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
386
474
|
],
|
|
387
475
|
};
|
|
388
476
|
}
|
|
389
|
-
return handleTraceVerticalSlice(repo, config, parsed.data);
|
|
477
|
+
return handleTraceVerticalSlice(ctx.repo, ctx.config, parsed.data);
|
|
390
478
|
} else if (name === "septum_clear_feature_context") {
|
|
391
479
|
const cleared = handleClearFeatureContext();
|
|
392
480
|
return {
|
|
@@ -412,7 +500,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
412
500
|
],
|
|
413
501
|
};
|
|
414
502
|
}
|
|
415
|
-
return handleCheckBoundary(evaluator, config, parsed.data);
|
|
503
|
+
return handleCheckBoundary(ctx.evaluator, ctx.config, parsed.data, ctx.repo);
|
|
416
504
|
} else if (name === "septum_get_symbol_hotspots") {
|
|
417
505
|
const parsed = GetSymbolHotspotsSchema.safeParse(args ?? {});
|
|
418
506
|
if (!parsed.success) {
|
|
@@ -426,7 +514,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
426
514
|
],
|
|
427
515
|
};
|
|
428
516
|
}
|
|
429
|
-
return handleGetSymbolHotspots(repo, config, parsed.data);
|
|
517
|
+
return handleGetSymbolHotspots(ctx.repo, ctx.config, parsed.data);
|
|
430
518
|
} else if (name === "septum_register_domain") {
|
|
431
519
|
const parsed = RegisterDomainSchema.safeParse(args ?? {});
|
|
432
520
|
if (!parsed.success) {
|
|
@@ -440,7 +528,21 @@ export async function runMCPServer(): Promise<void> {
|
|
|
440
528
|
],
|
|
441
529
|
};
|
|
442
530
|
}
|
|
443
|
-
return await handleRegisterDomain(repo, config, parsed.data);
|
|
531
|
+
return await handleRegisterDomain(ctx.repo, ctx.config, parsed.data);
|
|
532
|
+
} else if (name === "septum_get_environment_topology") {
|
|
533
|
+
const parsed = GetEnvironmentTopologySchema.safeParse(args ?? {});
|
|
534
|
+
if (!parsed.success) {
|
|
535
|
+
return {
|
|
536
|
+
isError: true,
|
|
537
|
+
content: [
|
|
538
|
+
{
|
|
539
|
+
type: "text",
|
|
540
|
+
text: `[Septum Validation Error] Invalid arguments for ${name}: ${parsed.error.message}`,
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
return handleGetEnvironmentTopology(ctx.repo, ctx.config, parsed.data, ctx.root);
|
|
444
546
|
} else {
|
|
445
547
|
throw new Error(`Unknown tool: '${name}'`);
|
|
446
548
|
}
|
|
@@ -468,7 +570,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
468
570
|
input: (args as Record<string, any>) ?? {},
|
|
469
571
|
metrics: { bytes_out: bytesOut, lines_out: linesOut },
|
|
470
572
|
session: sessionPayload,
|
|
471
|
-
status: response?.isError ? "error" : "success",
|
|
573
|
+
status: (response as { isError?: boolean } | undefined)?.isError ? "error" : "success",
|
|
472
574
|
});
|
|
473
575
|
|
|
474
576
|
return response;
|
|
@@ -522,7 +624,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
522
624
|
]);
|
|
523
625
|
|
|
524
626
|
try {
|
|
525
|
-
const bgPipeline =
|
|
627
|
+
const bgPipeline = initialCtx.pipeline;
|
|
526
628
|
const cwd = process.cwd();
|
|
527
629
|
|
|
528
630
|
fsWatcher = watch(cwd, { recursive: true }, (_event, filename) => {
|
|
@@ -569,13 +671,14 @@ export async function runMCPServer(): Promise<void> {
|
|
|
569
671
|
// Ignore watcher close errors during shutdown
|
|
570
672
|
}
|
|
571
673
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
674
|
+
for (const ctx of workspaceContextCache.values()) {
|
|
675
|
+
try {
|
|
676
|
+
ctx.db.close();
|
|
677
|
+
} catch (err) {
|
|
678
|
+
// Ignore close error during shutdown
|
|
679
|
+
}
|
|
578
680
|
}
|
|
681
|
+
console.error("[Septum MCP] All database connections closed cleanly.");
|
|
579
682
|
process.exit(0);
|
|
580
683
|
};
|
|
581
684
|
|
|
@@ -590,11 +693,12 @@ export async function runMCPServer(): Promise<void> {
|
|
|
590
693
|
// Ignore watcher close errors
|
|
591
694
|
}
|
|
592
695
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
696
|
+
for (const ctx of workspaceContextCache.values()) {
|
|
697
|
+
try {
|
|
698
|
+
ctx.db.close();
|
|
699
|
+
} catch {
|
|
700
|
+
// Ignore close error
|
|
701
|
+
}
|
|
598
702
|
}
|
|
599
703
|
});
|
|
600
704
|
|
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
import type { BoundaryEvaluator } from "../../core/boundary/evaluator.ts";
|
|
2
2
|
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
3
|
+
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
3
4
|
|
|
4
5
|
export interface CheckBoundaryArgs {
|
|
5
6
|
file_path?: string;
|
|
7
|
+
filePath?: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
path?: string;
|
|
6
10
|
file_paths?: string[];
|
|
11
|
+
filePaths?: string[];
|
|
12
|
+
files?: string[];
|
|
13
|
+
paths?: string[];
|
|
7
14
|
proposed_imports?: string[];
|
|
15
|
+
proposedImports?: string[];
|
|
16
|
+
imports?: string[];
|
|
8
17
|
feature_key?: string;
|
|
18
|
+
featureKey?: string;
|
|
19
|
+
feature?: string;
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
export function handleCheckBoundary(
|
|
12
23
|
evaluator: BoundaryEvaluator,
|
|
13
24
|
config: ValidatedSeptumConfig,
|
|
14
|
-
args: CheckBoundaryArgs
|
|
25
|
+
args: CheckBoundaryArgs,
|
|
26
|
+
repo?: SeptumRepository
|
|
15
27
|
) {
|
|
28
|
+
const rawFilePaths = args.file_paths ?? args.filePaths ?? args.files ?? args.paths;
|
|
29
|
+
const rawFilePath = args.file_path ?? args.filePath ?? args.file ?? args.path;
|
|
30
|
+
const proposedImports = args.proposed_imports ?? args.proposedImports ?? args.imports ?? [];
|
|
31
|
+
const featureKey = args.feature_key ?? args.featureKey ?? args.feature;
|
|
32
|
+
|
|
16
33
|
const filesToCheck: string[] = [];
|
|
17
|
-
if (
|
|
18
|
-
filesToCheck.push(...
|
|
19
|
-
} else if (
|
|
20
|
-
|
|
34
|
+
if (rawFilePaths && Array.isArray(rawFilePaths) && rawFilePaths.length > 0) {
|
|
35
|
+
filesToCheck.push(...rawFilePaths);
|
|
36
|
+
} else if (rawFilePath) {
|
|
37
|
+
if (Array.isArray(rawFilePath)) {
|
|
38
|
+
filesToCheck.push(...rawFilePath);
|
|
39
|
+
} else {
|
|
40
|
+
filesToCheck.push(rawFilePath);
|
|
41
|
+
}
|
|
21
42
|
} else {
|
|
22
43
|
throw new Error("Missing required argument: specify either 'file_path' or 'file_paths'");
|
|
23
44
|
}
|
|
@@ -25,11 +46,20 @@ export function handleCheckBoundary(
|
|
|
25
46
|
// Multi-file batch evaluation
|
|
26
47
|
if (filesToCheck.length > 1) {
|
|
27
48
|
const results = filesToCheck.map((filePath) => {
|
|
28
|
-
const violations = evaluator.evaluate(filePath,
|
|
49
|
+
const violations = evaluator.evaluate(filePath, proposedImports, featureKey, config);
|
|
50
|
+
const impact = repo?.getFileInboundImpact(filePath);
|
|
29
51
|
return {
|
|
30
52
|
file: filePath,
|
|
31
53
|
status: violations.length > 0 ? "rejected" : "approved",
|
|
32
54
|
violations,
|
|
55
|
+
impact_summary: impact
|
|
56
|
+
? {
|
|
57
|
+
direct_dependents: impact.direct_dependents_count,
|
|
58
|
+
top_consumers: impact.top_consumers,
|
|
59
|
+
symbols_count: impact.symbols_count,
|
|
60
|
+
risk_level: impact.risk_level,
|
|
61
|
+
}
|
|
62
|
+
: undefined,
|
|
33
63
|
};
|
|
34
64
|
});
|
|
35
65
|
|
|
@@ -60,7 +90,8 @@ export function handleCheckBoundary(
|
|
|
60
90
|
|
|
61
91
|
// Single file evaluation (backward-compatible output)
|
|
62
92
|
const singleFile = filesToCheck[0];
|
|
63
|
-
const violations = evaluator.evaluate(singleFile,
|
|
93
|
+
const violations = evaluator.evaluate(singleFile, proposedImports, featureKey, config);
|
|
94
|
+
const impact = repo?.getFileInboundImpact(singleFile);
|
|
64
95
|
|
|
65
96
|
if (violations.length > 0) {
|
|
66
97
|
return {
|
|
@@ -73,6 +104,14 @@ export function handleCheckBoundary(
|
|
|
73
104
|
file: singleFile,
|
|
74
105
|
violations_count: violations.length,
|
|
75
106
|
violations,
|
|
107
|
+
impact_summary: impact
|
|
108
|
+
? {
|
|
109
|
+
direct_dependents: impact.direct_dependents_count,
|
|
110
|
+
top_consumers: impact.top_consumers,
|
|
111
|
+
symbols_count: impact.symbols_count,
|
|
112
|
+
risk_level: impact.risk_level,
|
|
113
|
+
}
|
|
114
|
+
: undefined,
|
|
76
115
|
message: "Boundary violation detected. Proposed imports violate bounded context architecture.",
|
|
77
116
|
},
|
|
78
117
|
null,
|
|
@@ -91,7 +130,18 @@ export function handleCheckBoundary(
|
|
|
91
130
|
{
|
|
92
131
|
status: "approved",
|
|
93
132
|
file: singleFile,
|
|
94
|
-
|
|
133
|
+
impact_summary: impact
|
|
134
|
+
? {
|
|
135
|
+
direct_dependents: impact.direct_dependents_count,
|
|
136
|
+
top_consumers: impact.top_consumers,
|
|
137
|
+
symbols_count: impact.symbols_count,
|
|
138
|
+
risk_level: impact.risk_level,
|
|
139
|
+
}
|
|
140
|
+
: undefined,
|
|
141
|
+
message:
|
|
142
|
+
impact && impact.direct_dependents_count > 0
|
|
143
|
+
? `No boundary violations detected. Caution: ${impact.direct_dependents_count} dependent file(s) consume this file.`
|
|
144
|
+
: "No boundary violations detected. Proposed imports are permissible.",
|
|
95
145
|
},
|
|
96
146
|
null,
|
|
97
147
|
2
|
|
@@ -6,15 +6,45 @@ import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
|
6
6
|
export interface GetDomainCatalogArgs {
|
|
7
7
|
domain?: string;
|
|
8
8
|
archetype_filter?: string;
|
|
9
|
+
workspace_path?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function handleGetDomainCatalog(
|
|
12
13
|
repo: SeptumRepository,
|
|
13
14
|
config: ValidatedSeptumConfig,
|
|
14
|
-
args: GetDomainCatalogArgs
|
|
15
|
+
args: GetDomainCatalogArgs,
|
|
16
|
+
workspaceRoot?: string
|
|
15
17
|
) {
|
|
16
18
|
if (!args.domain) {
|
|
17
19
|
const allDomains = repo.getAllDomains();
|
|
20
|
+
if (allDomains.length === 0) {
|
|
21
|
+
const root = workspaceRoot || process.cwd();
|
|
22
|
+
const diagnostic = {
|
|
23
|
+
status: "NOT_INDEXED",
|
|
24
|
+
total_domains: 0,
|
|
25
|
+
message: `Workspace '${root}' belum memiliki domain terdaftar dalam Septum catalog.`,
|
|
26
|
+
suggested_action: {
|
|
27
|
+
tool: "septum_register_domain",
|
|
28
|
+
params: {
|
|
29
|
+
name: "core",
|
|
30
|
+
root: "src",
|
|
31
|
+
ingest_now: true,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
agent_guidance:
|
|
35
|
+
"Panggil tool 'septum_register_domain(name: \"core\", root: \"src\", ingest_now: true)' untuk mendaftarkan dan mengindeks domain utama secara otomatis, atau jalankan 'septum init' di terminal workspace.",
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text" as const,
|
|
41
|
+
text: JSON.stringify(diagnostic, null, 2),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
metadata: diagnostic,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
18
48
|
const projectType = repo.getMeta("project_type") || "generic";
|
|
19
49
|
const framework = repo.getMeta("framework") || "unknown";
|
|
20
50
|
const archStyle = repo.getMeta("architecture_style") || "modular";
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
2
|
+
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
3
|
+
import { EnvironmentDetector } from "../../core/topology/environment-detector.ts";
|
|
4
|
+
import type { EnvironmentLayer, ResolveLayerInput, ResolveNodeInput } from "../../core/topology/types.ts";
|
|
5
|
+
|
|
6
|
+
export interface GetEnvironmentTopologyArgs {
|
|
7
|
+
resolve?: ResolveLayerInput;
|
|
8
|
+
resolveNode?: ResolveNodeInput;
|
|
9
|
+
refresh?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function handleGetEnvironmentTopology(
|
|
13
|
+
repo: SeptumRepository,
|
|
14
|
+
_config: ValidatedSeptumConfig,
|
|
15
|
+
args: GetEnvironmentTopologyArgs,
|
|
16
|
+
workspaceRoot: string
|
|
17
|
+
) {
|
|
18
|
+
// If layer resolution input is provided, persist into SQLite SSOT
|
|
19
|
+
if (args.resolve) {
|
|
20
|
+
const existing = repo.environment.getLayer(args.resolve.layer);
|
|
21
|
+
repo.environment.saveLayer({
|
|
22
|
+
layer: args.resolve.layer,
|
|
23
|
+
platform: args.resolve.platform,
|
|
24
|
+
canonical_tag: EnvironmentDetector.matchCanonicalTag(args.resolve.platform),
|
|
25
|
+
status: "VERIFIED",
|
|
26
|
+
constraints: args.resolve.constraints && args.resolve.constraints.length > 0 ? args.resolve.constraints : (existing?.constraints ?? []),
|
|
27
|
+
detected_from: ["Manual Developer Resolution via Septum"],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If node resolution input is provided, persist node into SQLite SSOT
|
|
32
|
+
if (args.resolveNode) {
|
|
33
|
+
repo.environment.saveNode({
|
|
34
|
+
id: args.resolveNode.id,
|
|
35
|
+
layer: args.resolveNode.layer,
|
|
36
|
+
name: args.resolveNode.name,
|
|
37
|
+
platform: args.resolveNode.platform,
|
|
38
|
+
canonical_tag: args.resolveNode.canonical_tag || EnvironmentDetector.matchCanonicalTag(args.resolveNode.platform),
|
|
39
|
+
domain_or_ip: args.resolveNode.domain_or_ip,
|
|
40
|
+
role: args.resolveNode.role,
|
|
41
|
+
status: "VERIFIED",
|
|
42
|
+
detected_from: ["Manual Developer Resolution via Septum MCP"],
|
|
43
|
+
constraints: args.resolveNode.constraints ?? [],
|
|
44
|
+
connected_to: args.resolveNode.connected_to ?? [],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Detect current topology merging heuristics with SQLite SSOT
|
|
49
|
+
const topology = EnvironmentDetector.detect(workspaceRoot, repo, args.refresh);
|
|
50
|
+
|
|
51
|
+
// Format into high-density Markdown output
|
|
52
|
+
let output = `### π Septum Environment & Topology Plane\n\n`;
|
|
53
|
+
output += `> **Overall Status:** \`${topology.overall_status}\`\n`;
|
|
54
|
+
output += `> **Topology Mode:** \`${topology.is_distributed ? "DISTRIBUTED MULTI-NODE GRAPH" : "MONOLITHIC SINGLE-NODE"}\`\n`;
|
|
55
|
+
output += `> **Architecture:** ${topology.architectural_summary}\n\n`;
|
|
56
|
+
|
|
57
|
+
// Section 1: Distributed Nodes Graph
|
|
58
|
+
if (topology.nodes.length > 0) {
|
|
59
|
+
output += `#### π¦ Distributed Topology Nodes (${topology.nodes.length} entities):\n`;
|
|
60
|
+
output += `| Node ID | Layer | Platform / Specs | Role | Domain / Host | Connected To | Status |\n`;
|
|
61
|
+
output += `| :--- | :--- | :--- | :--- | :--- | :--- | :--- |\n`;
|
|
62
|
+
for (const node of topology.nodes) {
|
|
63
|
+
const statusBadge = node.status === "VERIFIED" ? "β
VERIFIED" : node.status === "DETECTED" ? "π DETECTED" : "β οΈ " + node.status;
|
|
64
|
+
const domain = node.domain_or_ip ? `\`${node.domain_or_ip}\`` : "-";
|
|
65
|
+
const role = node.role ? `\`${node.role}\`` : "-";
|
|
66
|
+
const connected = node.connected_to && node.connected_to.length > 0 ? node.connected_to.join(", ") : "-";
|
|
67
|
+
output += `| **\`${node.id}\`** | ${node.layer.toUpperCase()} | ${node.platform} | ${role} | ${domain} | ${connected} | ${statusBadge} |\n`;
|
|
68
|
+
}
|
|
69
|
+
output += `\n`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Section 2: 6-Layer Summary
|
|
73
|
+
output += `#### πΊοΈ 6-Layer Plane Summary:\n`;
|
|
74
|
+
output += `| Layer | Plane Domain | Platform / Setup | Status | Evidence / Files |\n`;
|
|
75
|
+
output += `| :--- | :--- | :--- | :--- | :--- |\n`;
|
|
76
|
+
|
|
77
|
+
const layerLabels: Record<EnvironmentLayer, string> = {
|
|
78
|
+
edge: "Perimeter & WAF",
|
|
79
|
+
gateway: "Ingress & Web Server",
|
|
80
|
+
host: "Deployment Platform",
|
|
81
|
+
runtime: "Application Engine",
|
|
82
|
+
storage: "Data & Cache",
|
|
83
|
+
telemetry: "Observability & Logs",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
for (const layerKey of Object.keys(topology.layers) as EnvironmentLayer[]) {
|
|
87
|
+
const info = topology.layers[layerKey];
|
|
88
|
+
const evidence = info.detected_from && info.detected_from.length > 0 ? info.detected_from.join(", ") : "-";
|
|
89
|
+
const statusBadge = info.status === "VERIFIED" ? "β
VERIFIED" : info.status === "DETECTED" ? "π DETECTED" : "β οΈ " + info.status;
|
|
90
|
+
output += `| **${layerKey.toUpperCase()}** | ${layerLabels[layerKey]} | ${info.platform || "Not Defined"} | ${statusBadge} | ${evidence} |\n`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Section 3: Active Constraints
|
|
94
|
+
output += `\n#### π Active Architectural Constraints:\n`;
|
|
95
|
+
let hasConstraints = false;
|
|
96
|
+
for (const node of topology.nodes) {
|
|
97
|
+
if (node.constraints && node.constraints.length > 0) {
|
|
98
|
+
hasConstraints = true;
|
|
99
|
+
output += `- **\`${node.id}\` (${node.platform}):**\n`;
|
|
100
|
+
for (const c of node.constraints) {
|
|
101
|
+
output += ` β’ ${c}\n`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!hasConstraints) {
|
|
106
|
+
output += `*No specific node constraints recorded.*\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Section 4: Cross-Layer & Distributed Interplays
|
|
110
|
+
if (topology.interplays.length > 0) {
|
|
111
|
+
output += `\n#### β‘ Cross-Layer & Distributed Invariants:\n`;
|
|
112
|
+
for (const interplay of topology.interplays) {
|
|
113
|
+
const nodeTrace = interplay.involved_node_ids && interplay.involved_node_ids.length > 0 ? ` (Nodes: \`${interplay.involved_node_ids.join(", ")}\`)` : "";
|
|
114
|
+
output += `- **${interplay.title}** (Layers: \`${interplay.involved_layers.join(" β ")}\`)${nodeTrace}:\n`;
|
|
115
|
+
output += ` β’ *Invarian:* ${interplay.risk_or_rule}\n`;
|
|
116
|
+
output += ` β’ *Tindakan Wajib:* ${interplay.required_action}\n`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Section 5: Pending Inquiries
|
|
121
|
+
if (topology.pending_inquiries.length > 0) {
|
|
122
|
+
output += `\n#### β Pending Architectural Inquiries (Self-Aware Inquiry Required):\n`;
|
|
123
|
+
output += `Terdapat layer atau node arsitektural yang belum terkonfirmasi. Sebelum mengeksekusi kode terkait deployment/throttling/cron/cookies, klarifikasikan hal berikut ke pengguna:\n`;
|
|
124
|
+
for (const q of topology.pending_inquiries) {
|
|
125
|
+
output += `- ${q}\n`;
|
|
126
|
+
}
|
|
127
|
+
output += `\n*Untuk mengunci jawaban pengguna ke SQLite SSOT, panggil kembali tool ini dengan argumen:*\n`;
|
|
128
|
+
output += `\`septum_get_environment_topology(resolve: { layer: "<layer_name>", platform: "<platform_name>" })\`\n`;
|
|
129
|
+
output += `*atau untuk entitas terdistribusi:*\n`;
|
|
130
|
+
output += `\`septum_get_environment_topology(resolveNode: { id: "<node_id>", layer: "<layer>", name: "<name>", platform: "<platform>", domain_or_ip: "<domain_or_ip>", role: "<role>" })\`\n`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text" as const,
|
|
137
|
+
text: output,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -4,9 +4,11 @@ import type { SymbolKind } from "../../types/index.ts";
|
|
|
4
4
|
|
|
5
5
|
export interface GetSymbolHotspotsArgs {
|
|
6
6
|
min_lines?: number;
|
|
7
|
-
|
|
7
|
+
min_nesting?: number;
|
|
8
|
+
kind?: SymbolKind | string;
|
|
8
9
|
domain?: string;
|
|
9
10
|
limit?: number;
|
|
11
|
+
sort_by?: "lines" | "nesting" | "risk_score";
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function handleGetSymbolHotspots(
|
|
@@ -14,23 +16,31 @@ export function handleGetSymbolHotspots(
|
|
|
14
16
|
_config: ValidatedSeptumConfig,
|
|
15
17
|
args: GetSymbolHotspotsArgs
|
|
16
18
|
) {
|
|
17
|
-
const minLines = args.min_lines ?? 30;
|
|
19
|
+
const minLines = args.min_lines ?? (args.min_nesting ? 0 : 30);
|
|
20
|
+
const minNesting = args.min_nesting ?? 1;
|
|
18
21
|
const limit = args.limit ?? 30;
|
|
22
|
+
const sortBy = args.sort_by ?? "risk_score";
|
|
19
23
|
|
|
20
24
|
const hotspots = repo.getHotspotSymbols({
|
|
21
25
|
minLines,
|
|
22
|
-
|
|
26
|
+
minNesting,
|
|
27
|
+
kind: args.kind as SymbolKind | undefined,
|
|
23
28
|
domain: args.domain,
|
|
24
29
|
limit,
|
|
30
|
+
sortBy,
|
|
25
31
|
});
|
|
26
32
|
|
|
27
33
|
const payload = {
|
|
28
34
|
total_found: hotspots.length,
|
|
29
35
|
threshold_min_lines: minLines,
|
|
36
|
+
threshold_min_nesting: minNesting,
|
|
37
|
+
sort_by: sortBy,
|
|
30
38
|
hotspots: hotspots.map((h) => ({
|
|
31
39
|
name: h.name,
|
|
32
40
|
kind: h.kind,
|
|
33
41
|
line_count: h.line_count,
|
|
42
|
+
nesting_depth: (h as any).nesting_depth ?? 1,
|
|
43
|
+
risk_score: (h as any).risk_score ?? h.line_count,
|
|
34
44
|
line_span: `${h.line_start}-${h.line_end}`,
|
|
35
45
|
file_path: h.file_path,
|
|
36
46
|
domain: h.domain_name,
|