septum 0.1.2 → 0.1.3

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.
@@ -1,68 +1,238 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const GetDomainCatalogSchema = z.object({
4
- domain: z.string().optional(),
5
- archetype: z.string().optional(),
6
- });
3
+ function withAliases(raw: unknown, aliases: Record<string, string>): unknown {
4
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
5
+ const obj = { ...(raw as Record<string, unknown>) };
6
+ for (const [alias, target] of Object.entries(aliases)) {
7
+ if (obj[alias] !== undefined && obj[target] === undefined) {
8
+ obj[target] = obj[alias];
9
+ }
10
+ }
11
+ return obj;
12
+ }
13
+
14
+ function normalizeBoundaryArgs(raw: unknown): unknown {
15
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
16
+ const obj = { ...(raw as Record<string, unknown>) };
17
+ const aliases: Record<string, string> = {
18
+ filePath: "file_path",
19
+ file: "file_path",
20
+ path: "file_path",
21
+ filePaths: "file_paths",
22
+ files: "file_paths",
23
+ paths: "file_paths",
24
+ proposedImports: "proposed_imports",
25
+ imports: "proposed_imports",
26
+ featureKey: "feature_key",
27
+ feature: "feature_key",
28
+ };
29
+ for (const [alias, target] of Object.entries(aliases)) {
30
+ if (obj[alias] !== undefined && obj[target] === undefined) {
31
+ obj[target] = obj[alias];
32
+ }
33
+ }
34
+ if (Array.isArray(obj.file_path) && !obj.file_paths) {
35
+ obj.file_paths = obj.file_path;
36
+ delete obj.file_path;
37
+ }
38
+ if (typeof obj.file_paths === "string" && !obj.file_path) {
39
+ obj.file_path = obj.file_paths;
40
+ delete obj.file_paths;
41
+ }
42
+ return obj;
43
+ }
44
+
45
+ export const GetDomainCatalogSchema = z.preprocess(
46
+ (args) =>
47
+ withAliases(args, {
48
+ domain_name: "domain",
49
+ domainName: "domain",
50
+ name: "domain",
51
+ archetype_filter: "archetype",
52
+ archetypeFilter: "archetype",
53
+ type: "archetype",
54
+ workspace: "workspace_path",
55
+ workspacePath: "workspace_path",
56
+ project_root: "workspace_path",
57
+ target_path: "workspace_path",
58
+ path: "workspace_path",
59
+ }),
60
+ z.object({
61
+ domain: z.string().optional(),
62
+ archetype: z.string().optional(),
63
+ workspace_path: z.string().optional(),
64
+ })
65
+ );
7
66
  export type GetDomainCatalogArgs = z.infer<typeof GetDomainCatalogSchema>;
8
67
 
9
- export const GetFeatureContextSchema = z.object({
10
- feature: z.string().min(1, "feature is required"),
11
- });
68
+ export const GetFeatureContextSchema = z.preprocess(
69
+ (args) =>
70
+ withAliases(args, {
71
+ feature_key: "feature",
72
+ featureKey: "feature",
73
+ name: "feature",
74
+ workspace: "workspace_path",
75
+ workspacePath: "workspace_path",
76
+ }),
77
+ z.object({
78
+ feature: z.string().min(1, "feature is required"),
79
+ workspace_path: z.string().optional(),
80
+ })
81
+ );
12
82
  export type GetFeatureContextArgs = z.infer<typeof GetFeatureContextSchema>;
13
83
 
14
- export const LocateSymbolSchema = z.object({
15
- query: z.string().min(1, "query is required"),
16
- domain: z.string().optional(),
17
- limit: z.number().int().positive().optional().default(10),
18
- });
84
+ export const LocateSymbolSchema = z.preprocess(
85
+ (args) =>
86
+ withAliases(args, {
87
+ symbol: "query",
88
+ name: "query",
89
+ symbol_name: "query",
90
+ symbolName: "query",
91
+ target: "query",
92
+ domain_name: "domain",
93
+ domainName: "domain",
94
+ max: "limit",
95
+ workspace: "workspace_path",
96
+ workspacePath: "workspace_path",
97
+ project_root: "workspace_path",
98
+ target_path: "workspace_path",
99
+ path: "workspace_path",
100
+ }),
101
+ z.object({
102
+ query: z.string().min(1, "query is required"),
103
+ domain: z.string().optional(),
104
+ limit: z.number().int().positive().optional().default(10),
105
+ workspace_path: z.string().optional(),
106
+ })
107
+ );
19
108
  export type LocateSymbolArgs = z.infer<typeof LocateSymbolSchema>;
20
109
 
21
- export const GetSymbolSchema = z.object({
22
- symbol: z.string().min(1, "symbol is required"),
23
- domain: z.string().optional(),
24
- include_dependencies: z.boolean().optional().default(true),
25
- });
110
+ export const GetSymbolSchema = z.preprocess(
111
+ (args) =>
112
+ withAliases(args, {
113
+ query: "symbol",
114
+ name: "symbol",
115
+ symbol_name: "symbol",
116
+ symbolName: "symbol",
117
+ target: "symbol",
118
+ domain_name: "domain",
119
+ domainName: "domain",
120
+ includeDependencies: "include_dependencies",
121
+ include_deps: "include_dependencies",
122
+ includeDeps: "include_dependencies",
123
+ dependencies: "include_dependencies",
124
+ deps: "include_dependencies",
125
+ }),
126
+ z.object({
127
+ symbol: z.string().min(1, "symbol is required"),
128
+ domain: z.string().optional(),
129
+ include_dependencies: z.boolean().optional().default(true),
130
+ })
131
+ );
26
132
  export type GetSymbolArgs = z.infer<typeof GetSymbolSchema>;
27
133
 
28
- export const GetSymbolImpactSchema = z.object({
29
- symbol: z.string().min(1, "symbol is required"),
30
- });
134
+ export const GetSymbolImpactSchema = z.preprocess(
135
+ (args) =>
136
+ withAliases(args, {
137
+ query: "symbol",
138
+ name: "symbol",
139
+ symbol_name: "symbol",
140
+ symbolName: "symbol",
141
+ file_path: "symbol",
142
+ filePath: "symbol",
143
+ file: "symbol",
144
+ path: "symbol",
145
+ target: "symbol",
146
+ }),
147
+ z.object({
148
+ symbol: z.string().min(1, "symbol is required"),
149
+ })
150
+ );
31
151
  export type GetSymbolImpactArgs = z.infer<typeof GetSymbolImpactSchema>;
32
152
 
33
- export const TraceVerticalSliceSchema = z.object({
34
- query: z.string().min(1, "query is required"),
35
- max_depth: z.number().int().positive().optional().default(5),
36
- });
153
+ export const TraceVerticalSliceSchema = z.preprocess(
154
+ (args) =>
155
+ withAliases(args, {
156
+ route: "query",
157
+ uri: "query",
158
+ route_uri: "query",
159
+ routeUri: "query",
160
+ symbol: "query",
161
+ name: "query",
162
+ feature: "query",
163
+ feature_key: "query",
164
+ featureKey: "query",
165
+ maxDepth: "max_depth",
166
+ depth: "max_depth",
167
+ }),
168
+ z.object({
169
+ query: z.string().min(1, "query is required"),
170
+ max_depth: z.number().int().positive().optional().default(5),
171
+ })
172
+ );
37
173
  export type TraceVerticalSliceArgs = z.infer<typeof TraceVerticalSliceSchema>;
38
174
 
39
- export const CheckBoundarySchema = z.object({
40
- file_path: z.string().optional(),
41
- file_paths: z.array(z.string()).optional(),
42
- proposed_imports: z.array(z.string()).default([]),
43
- feature_key: z.string().optional(),
44
- });
175
+ export const CheckBoundarySchema = z.preprocess(
176
+ normalizeBoundaryArgs,
177
+ z.object({
178
+ file_path: z.string().optional(),
179
+ file_paths: z.array(z.string()).optional(),
180
+ proposed_imports: z.array(z.string()).default([]),
181
+ feature_key: z.string().optional(),
182
+ })
183
+ );
45
184
  export type CheckBoundaryArgs = z.infer<typeof CheckBoundarySchema>;
46
185
 
47
- export const GetSymbolHotspotsSchema = z.object({
48
- domain: z.string().optional(),
49
- limit: z.number().int().positive().optional().default(10),
50
- });
186
+ export const GetSymbolHotspotsSchema = z.preprocess(
187
+ (args) =>
188
+ withAliases(args, {
189
+ domain_name: "domain",
190
+ domainName: "domain",
191
+ max: "limit",
192
+ }),
193
+ z.object({
194
+ domain: z.string().optional(),
195
+ limit: z.number().int().positive().optional().default(10),
196
+ })
197
+ );
51
198
  export type GetSymbolHotspotsArgs = z.infer<typeof GetSymbolHotspotsSchema>;
52
199
 
53
- export const RegisterDomainSchema = z.object({
54
- name: z.string().min(1, "Domain name is required"),
55
- root: z.string().min(1, "Domain root path is required"),
56
- description: z.string().optional(),
57
- allowed_dependencies: z.array(z.string()).optional(),
58
- forbidden_dependencies: z.array(z.string()).optional(),
59
- archetypes: z.record(z.string()).optional(),
60
- ingest_now: z.boolean().optional().default(false),
61
- });
200
+ export const RegisterDomainSchema = z.preprocess(
201
+ (args) =>
202
+ withAliases(args, {
203
+ domain: "name",
204
+ domain_name: "name",
205
+ domainName: "name",
206
+ root_path: "root",
207
+ rootPath: "root",
208
+ path: "root",
209
+ allowedDependencies: "allowed_dependencies",
210
+ allowed: "allowed_dependencies",
211
+ forbiddenDependencies: "forbidden_dependencies",
212
+ forbidden: "forbidden_dependencies",
213
+ archetype: "archetypes",
214
+ ingestNow: "ingest_now",
215
+ }),
216
+ z.object({
217
+ name: z.string().min(1, "Domain name is required"),
218
+ root: z.string().min(1, "Domain root path is required"),
219
+ description: z.string().optional(),
220
+ allowed_dependencies: z.array(z.string()).optional(),
221
+ forbidden_dependencies: z.array(z.string()).optional(),
222
+ archetypes: z.record(z.string()).optional(),
223
+ ingest_now: z.boolean().optional().default(false),
224
+ })
225
+ );
62
226
  export type RegisterDomainArgs = z.infer<typeof RegisterDomainSchema>;
63
227
 
64
- export const AutoDiscoverDomainsSchema = z.object({
65
- persist: z.boolean().optional().default(true),
66
- });
228
+ export const AutoDiscoverDomainsSchema = z.preprocess(
229
+ (args) =>
230
+ withAliases(args, {
231
+ save: "persist",
232
+ }),
233
+ z.object({
234
+ persist: z.boolean().optional().default(true),
235
+ })
236
+ );
67
237
  export type AutoDiscoverDomainsArgs = z.infer<typeof AutoDiscoverDomainsSchema>;
68
238
 
package/src/mcp/server.ts CHANGED
@@ -54,12 +54,51 @@ import {
54
54
  TraceVerticalSliceSchema,
55
55
  } from "./schemas.ts";
56
56
  import { SEPTUM_VERSION } from "../version.ts";
57
+ import { resolveWorkspaceRoot } from "../core/resolver/path-resolver.ts";
58
+ import type { ValidatedSeptumConfig } from "../core/config/schema.ts";
57
59
 
58
- export async function runMCPServer(): Promise<void> {
59
- const config = ConfigLoader.load();
60
+ interface WorkspaceContext {
61
+ root: string;
62
+ config: ValidatedSeptumConfig;
63
+ db: SeptumDatabase;
64
+ repo: SeptumRepository;
65
+ evaluator: BoundaryEvaluator;
66
+ pipeline: IngestionPipeline;
67
+ }
68
+
69
+ const workspaceContextCache = new Map<string, WorkspaceContext>();
70
+
71
+ function extractPathHint(args: Record<string, any> | undefined): string | undefined {
72
+ if (!args) return undefined;
73
+ if (typeof args.workspace_path === "string" && args.workspace_path) return args.workspace_path;
74
+ if (typeof args.target_path === "string" && args.target_path) return args.target_path;
75
+ if (typeof args.project_root === "string" && args.project_root) return args.project_root;
76
+ if (typeof args.file_path === "string" && args.file_path) return args.file_path;
77
+ if (Array.isArray(args.file_paths) && args.file_paths[0]) return args.file_paths[0];
78
+ if (typeof args.root === "string" && args.root) return args.root;
79
+ return undefined;
80
+ }
81
+
82
+ function getWorkspaceContext(pathHint?: string): WorkspaceContext {
83
+ const root = resolveWorkspaceRoot(pathHint);
84
+ let ctx = workspaceContextCache.get(root);
85
+ if (ctx) {
86
+ return ctx;
87
+ }
88
+
89
+ const config = ConfigLoader.load(undefined, root);
60
90
  const db = new SeptumDatabase(config.settings.db_path);
61
91
  const repo = new SeptumRepository(db.raw);
62
92
  const evaluator = new BoundaryEvaluator(repo);
93
+ const pipeline = new IngestionPipeline(repo, root);
94
+
95
+ ctx = { root, config, db, repo, evaluator, pipeline };
96
+ workspaceContextCache.set(root, ctx);
97
+ return ctx;
98
+ }
99
+
100
+ export async function runMCPServer(): Promise<void> {
101
+ const initialCtx = getWorkspaceContext();
63
102
 
64
103
  const server = new Server(
65
104
  {
@@ -297,7 +336,10 @@ export async function runMCPServer(): Promise<void> {
297
336
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
298
337
  const { name, arguments: args } = request.params;
299
338
  const startTime = performance.now();
300
- const activeSession = SessionManager.getActiveSession(process.cwd());
339
+ const pathHint = extractPathHint(args as Record<string, any>);
340
+ const ctx = getWorkspaceContext(pathHint);
341
+
342
+ const activeSession = SessionManager.getActiveSession(ctx.root);
301
343
  const sessionPayload = activeSession
302
344
  ? { feature_key: activeSession.feature_key, domain: activeSession.domain }
303
345
  : null;
@@ -316,7 +358,7 @@ export async function runMCPServer(): Promise<void> {
316
358
  ],
317
359
  };
318
360
  }
319
- return handleGetDomainCatalog(repo, config, parsed.data);
361
+ return handleGetDomainCatalog(ctx.repo, ctx.config, parsed.data, ctx.root);
320
362
  } else if (name === "septum_get_feature_context") {
321
363
  const parsed = GetFeatureContextSchema.safeParse(args ?? {});
322
364
  if (!parsed.success) {
@@ -330,7 +372,7 @@ export async function runMCPServer(): Promise<void> {
330
372
  ],
331
373
  };
332
374
  }
333
- return handleGetFeatureContext(repo, config, parsed.data);
375
+ return handleGetFeatureContext(ctx.repo, ctx.config, parsed.data);
334
376
  } else if (name === "septum_locate_symbol") {
335
377
  const parsed = LocateSymbolSchema.safeParse(args ?? {});
336
378
  if (!parsed.success) {
@@ -344,7 +386,7 @@ export async function runMCPServer(): Promise<void> {
344
386
  ],
345
387
  };
346
388
  }
347
- return handleLocateSymbol(repo, config, parsed.data);
389
+ return await handleLocateSymbol(ctx.repo, ctx.config, parsed.data, ctx.pipeline, ctx.root);
348
390
  } else if (name === "septum_get_symbol") {
349
391
  const parsed = GetSymbolSchema.safeParse(args ?? {});
350
392
  if (!parsed.success) {
@@ -358,7 +400,7 @@ export async function runMCPServer(): Promise<void> {
358
400
  ],
359
401
  };
360
402
  }
361
- return handleGetSymbol(repo, config, parsed.data);
403
+ return handleGetSymbol(ctx.repo, ctx.config, parsed.data);
362
404
  } else if (name === "septum_get_symbol_impact") {
363
405
  const parsed = GetSymbolImpactSchema.safeParse(args ?? {});
364
406
  if (!parsed.success) {
@@ -372,7 +414,7 @@ export async function runMCPServer(): Promise<void> {
372
414
  ],
373
415
  };
374
416
  }
375
- return handleGetSymbolImpact(repo, config, parsed.data);
417
+ return handleGetSymbolImpact(ctx.repo, ctx.config, parsed.data);
376
418
  } else if (name === "septum_trace_vertical_slice") {
377
419
  const parsed = TraceVerticalSliceSchema.safeParse(args ?? {});
378
420
  if (!parsed.success) {
@@ -386,7 +428,7 @@ export async function runMCPServer(): Promise<void> {
386
428
  ],
387
429
  };
388
430
  }
389
- return handleTraceVerticalSlice(repo, config, parsed.data);
431
+ return handleTraceVerticalSlice(ctx.repo, ctx.config, parsed.data);
390
432
  } else if (name === "septum_clear_feature_context") {
391
433
  const cleared = handleClearFeatureContext();
392
434
  return {
@@ -412,7 +454,7 @@ export async function runMCPServer(): Promise<void> {
412
454
  ],
413
455
  };
414
456
  }
415
- return handleCheckBoundary(evaluator, config, parsed.data);
457
+ return handleCheckBoundary(ctx.evaluator, ctx.config, parsed.data, ctx.repo);
416
458
  } else if (name === "septum_get_symbol_hotspots") {
417
459
  const parsed = GetSymbolHotspotsSchema.safeParse(args ?? {});
418
460
  if (!parsed.success) {
@@ -426,7 +468,7 @@ export async function runMCPServer(): Promise<void> {
426
468
  ],
427
469
  };
428
470
  }
429
- return handleGetSymbolHotspots(repo, config, parsed.data);
471
+ return handleGetSymbolHotspots(ctx.repo, ctx.config, parsed.data);
430
472
  } else if (name === "septum_register_domain") {
431
473
  const parsed = RegisterDomainSchema.safeParse(args ?? {});
432
474
  if (!parsed.success) {
@@ -440,7 +482,7 @@ export async function runMCPServer(): Promise<void> {
440
482
  ],
441
483
  };
442
484
  }
443
- return await handleRegisterDomain(repo, config, parsed.data);
485
+ return await handleRegisterDomain(ctx.repo, ctx.config, parsed.data);
444
486
  } else {
445
487
  throw new Error(`Unknown tool: '${name}'`);
446
488
  }
@@ -522,7 +564,7 @@ export async function runMCPServer(): Promise<void> {
522
564
  ]);
523
565
 
524
566
  try {
525
- const bgPipeline = new IngestionPipeline(repo);
567
+ const bgPipeline = initialCtx.pipeline;
526
568
  const cwd = process.cwd();
527
569
 
528
570
  fsWatcher = watch(cwd, { recursive: true }, (_event, filename) => {
@@ -569,13 +611,14 @@ export async function runMCPServer(): Promise<void> {
569
611
  // Ignore watcher close errors during shutdown
570
612
  }
571
613
  }
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}`);
614
+ for (const ctx of workspaceContextCache.values()) {
615
+ try {
616
+ ctx.db.close();
617
+ } catch (err) {
618
+ // Ignore close error during shutdown
619
+ }
578
620
  }
621
+ console.error("[Septum MCP] All database connections closed cleanly.");
579
622
  process.exit(0);
580
623
  };
581
624
 
@@ -590,11 +633,12 @@ export async function runMCPServer(): Promise<void> {
590
633
  // Ignore watcher close errors
591
634
  }
592
635
  }
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}`);
636
+ for (const ctx of workspaceContextCache.values()) {
637
+ try {
638
+ ctx.db.close();
639
+ } catch {
640
+ // Ignore close error
641
+ }
598
642
  }
599
643
  });
600
644
 
@@ -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";