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.
- package/README.md +2 -0
- package/package.json +1 -1
- 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/file.repository.ts +7 -0
- package/src/core/database/repository.ts +13 -0
- package/src/core/discovery/topology-detector.ts +71 -3
- package/src/core/ingestion/pipeline.ts +98 -1
- package/src/core/parser/extractors/base.ts +1 -0
- package/src/core/parser/extractors/blade.ts +158 -0
- package/src/core/parser/extractors/laravel-semantic.ts +186 -27
- package/src/core/parser/extractors/registry.ts +21 -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 +22 -7
- package/src/mcp/schemas.ts +216 -46
- package/src/mcp/server.ts +68 -24
- package/src/mcp/tools/check-boundary.ts +58 -8
- package/src/mcp/tools/get-domain-catalog.ts +31 -1
- package/src/mcp/tools/locate-symbol.ts +119 -3
- package/src/mcp/tools/trace-vertical-slice.ts +2 -1
- package/src/types/index.ts +4 -1
package/src/mcp/schemas.ts
CHANGED
|
@@ -1,68 +1,238 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
10
|
-
|
|
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.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
29
|
-
|
|
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.
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
65
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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 (
|
|
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";
|