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.
Files changed (46) hide show
  1. package/README.md +7 -2
  2. package/package.json +1 -1
  3. package/src/cli/commands/slice.ts +4 -46
  4. package/src/cli/commands/topology.ts +119 -0
  5. package/src/cli/index.ts +11 -0
  6. package/src/cli/templates/skill.ts +7 -4
  7. package/src/core/config/loader.ts +21 -8
  8. package/src/core/database/repositories/dependency.repository.ts +70 -0
  9. package/src/core/database/repositories/environment.repository.ts +207 -0
  10. package/src/core/database/repositories/file.repository.ts +7 -0
  11. package/src/core/database/repositories/symbol.repository.ts +28 -9
  12. package/src/core/database/repository.ts +19 -1
  13. package/src/core/database/schema.ts +153 -120
  14. package/src/core/discovery/topology-detector.ts +71 -3
  15. package/src/core/ingestion/pipeline.ts +98 -1
  16. package/src/core/parser/behavioral-analyzer.ts +119 -0
  17. package/src/core/parser/boundary-tracker.ts +160 -0
  18. package/src/core/parser/extractors/base.ts +2 -0
  19. package/src/core/parser/extractors/blade.ts +158 -0
  20. package/src/core/parser/extractors/go.ts +5 -1
  21. package/src/core/parser/extractors/laravel-semantic.ts +506 -31
  22. package/src/core/parser/extractors/php.ts +5 -1
  23. package/src/core/parser/extractors/python.ts +3 -1
  24. package/src/core/parser/extractors/registry.ts +21 -0
  25. package/src/core/parser/extractors/typescript.ts +5 -1
  26. package/src/core/parser/schema/providers/laravel-migration-provider.ts +215 -0
  27. package/src/core/parser/schema/providers/prisma-schema-provider.ts +89 -0
  28. package/src/core/parser/schema/schema-provider-registry.ts +43 -0
  29. package/src/core/parser/schema/schema-provider.interface.ts +33 -0
  30. package/src/core/parser/tree-sitter.ts +2 -0
  31. package/src/core/resolver/path-resolver.ts +147 -0
  32. package/src/core/resolver/vertical-slice-tracer.ts +56 -8
  33. package/src/core/topology/environment-detector.ts +860 -0
  34. package/src/core/topology/types.ts +83 -0
  35. package/src/index.ts +3 -0
  36. package/src/mcp/schemas.ts +264 -46
  37. package/src/mcp/server.ts +130 -26
  38. package/src/mcp/tools/check-boundary.ts +58 -8
  39. package/src/mcp/tools/get-domain-catalog.ts +31 -1
  40. package/src/mcp/tools/get-environment-topology.ts +141 -0
  41. package/src/mcp/tools/get-symbol-hotspots.ts +13 -3
  42. package/src/mcp/tools/locate-symbol.ts +119 -3
  43. package/src/mcp/tools/register-domain.ts +1 -1
  44. package/src/mcp/tools/trace-vertical-slice.ts +11 -65
  45. package/src/types/index.ts +20 -1
  46. 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
- export async function runMCPServer(): Promise<void> {
59
- const config = ConfigLoader.load();
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 (default: 30 lines). Essential for Single Responsibility Principle (SRP) enforcement, finding God Functions, and isolating refactoring targets deterministically without reading raw files.",
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 activeSession = SessionManager.getActiveSession(process.cwd());
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 = new IngestionPipeline(repo);
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
- try {
573
- db.close();
574
- console.error("[Septum MCP] Database connection closed cleanly.");
575
- } catch (err) {
576
- const msg = err instanceof Error ? err.message : String(err);
577
- console.error(`[Septum MCP] Error closing database during cleanup: ${msg}`);
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
- try {
594
- db.close();
595
- } catch (err) {
596
- const msg = err instanceof Error ? err.message : String(err);
597
- console.error(`[Septum MCP] Error closing database on beforeExit: ${msg}`);
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 (args.file_paths && Array.isArray(args.file_paths) && args.file_paths.length > 0) {
18
- filesToCheck.push(...args.file_paths);
19
- } else if (args.file_path) {
20
- filesToCheck.push(args.file_path);
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, args.proposed_imports ?? [], args.feature_key, config);
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, args.proposed_imports ?? [], args.feature_key, config);
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
- message: "No boundary violations detected. Proposed imports are permissible.",
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
- kind?: SymbolKind;
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
- kind: args.kind,
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,