septum 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -55
- package/package.json +3 -2
- package/src/cli/commands/check.ts +2 -0
- package/src/cli/commands/hook.ts +59 -12
- package/src/cli/commands/init.ts +440 -121
- package/src/cli/commands/sync.ts +27 -17
- package/src/cli/commands/topology.ts +89 -47
- package/src/cli/index.ts +5 -67
- package/src/cli/scaffold/harness-config.ts +697 -0
- package/src/cli/templates/hooks.ts +6 -6
- package/src/cli/templates/rules.ts +8 -8
- package/src/cli/templates/skill.ts +11 -18
- package/src/cli/ui/prompts.ts +122 -0
- package/src/core/boundary/staged-auditor.ts +5 -2
- package/src/core/database/client.ts +8 -0
- package/src/core/database/repositories/dependency.repository.ts +77 -13
- package/src/core/database/repositories/domain.repository.ts +86 -33
- package/src/core/database/repositories/symbol.repository.ts +128 -117
- package/src/core/database/repository.ts +17 -5
- package/src/core/database/schema.ts +1 -1
- package/src/core/discovery/topology-detector.ts +71 -19
- package/src/core/ingestion/pipeline.ts +228 -94
- package/src/core/parser/extractors/laravel-semantic.ts +9 -8
- package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
- package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
- package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
- package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
- package/src/core/resolver/call-graph-tracer.ts +4 -3
- package/src/core/resolver/path-resolver.ts +57 -4
- package/src/core/resources/ignore.ts +35 -0
- package/src/core/resources/limits.ts +14 -0
- package/src/core/resources/source-files.ts +62 -0
- package/src/core/resources/work-queue.ts +74 -0
- package/src/core/session/session-manager.ts +19 -6
- package/src/core/telemetry/telemetry.ts +12 -1
- package/src/core/topology/environment-detector.ts +21 -11
- package/src/mcp/schemas.ts +381 -8
- package/src/mcp/server.ts +321 -535
- package/src/mcp/tools/boundary.ts +17 -0
- package/src/mcp/tools/check-boundary.ts +83 -11
- package/src/mcp/tools/domain.ts +40 -0
- package/src/mcp/tools/feature.ts +67 -0
- package/src/mcp/tools/get-domain-catalog.ts +9 -6
- package/src/mcp/tools/get-environment-topology.ts +2 -2
- package/src/mcp/tools/get-feature-context.ts +9 -12
- package/src/mcp/tools/get-symbol-impact.ts +5 -2
- package/src/mcp/tools/get-symbol.ts +2 -1
- package/src/mcp/tools/locate-symbol.ts +3 -3
- package/src/mcp/tools/register-domain.ts +25 -9
- package/src/mcp/tools/register-feature.ts +34 -17
- package/src/mcp/tools/slice.ts +15 -0
- package/src/mcp/tools/symbol.ts +82 -0
- package/src/mcp/workspace-manager.ts +159 -0
- package/src/mcp/workspace-watcher.ts +202 -0
- package/src/types/index.ts +1 -0
- package/src/cli/commands/ingest.ts +0 -29
- package/src/cli/commands/locate.ts +0 -61
- package/src/cli/commands/query.ts +0 -104
- package/src/cli/commands/slice.ts +0 -50
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BoundaryEvaluator } from "../../core/boundary/evaluator.ts";
|
|
2
|
+
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
3
|
+
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
4
|
+
import type { BoundaryToolArgs } from "../schemas.ts";
|
|
5
|
+
import { handleCheckBoundary } from "./check-boundary.ts";
|
|
6
|
+
|
|
7
|
+
export type { BoundaryToolArgs };
|
|
8
|
+
|
|
9
|
+
export async function handleBoundaryTool(
|
|
10
|
+
evaluator: BoundaryEvaluator,
|
|
11
|
+
config: ValidatedSeptumConfig,
|
|
12
|
+
args: BoundaryToolArgs,
|
|
13
|
+
repo?: SeptumRepository,
|
|
14
|
+
workspaceRoot?: string
|
|
15
|
+
) {
|
|
16
|
+
return await handleCheckBoundary(evaluator, config, args as any, repo, workspaceRoot);
|
|
17
|
+
}
|
|
@@ -3,8 +3,9 @@ import { auditStagedFiles } from "../../core/boundary/staged-auditor.ts";
|
|
|
3
3
|
import type { BoundaryEvaluator } from "../../core/boundary/evaluator.ts";
|
|
4
4
|
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
5
5
|
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
6
|
+
import { SessionManager } from "../../core/session/session-manager.ts";
|
|
6
7
|
import type { BoundaryViolation } from "../../types/index.ts";
|
|
7
|
-
import {
|
|
8
|
+
import { CheckBoundarySchema, type CheckBoundaryArgs } from "../schemas.ts";
|
|
8
9
|
|
|
9
10
|
export type { CheckBoundaryArgs } from "../schemas.ts";
|
|
10
11
|
|
|
@@ -15,11 +16,56 @@ export async function handleCheckBoundary(
|
|
|
15
16
|
repo?: SeptumRepository,
|
|
16
17
|
workspaceRoot?: string
|
|
17
18
|
) {
|
|
18
|
-
const args = (
|
|
19
|
+
const args = CheckBoundarySchema.parse(rawArgs ?? {});
|
|
20
|
+
if (args.domain) {
|
|
21
|
+
const domain = Object.keys(config.domains || {}).find(name => name.toLowerCase() === args.domain!.toLowerCase());
|
|
22
|
+
if (!domain) throw new Error(`Unknown boundary domain '${args.domain}'. Choose a registered domain.`);
|
|
23
|
+
args.domain = domain;
|
|
24
|
+
}
|
|
19
25
|
const root = workspaceRoot || args.workspace_path || process.cwd();
|
|
20
26
|
const proposedImports = args.proposed_imports ?? [];
|
|
21
|
-
const featureKey = args.feature_key;
|
|
22
27
|
|
|
28
|
+
const activeSession = SessionManager.getActiveSession(root);
|
|
29
|
+
|
|
30
|
+
let effectiveFeatureKey = args.feature_key;
|
|
31
|
+
if (activeSession) {
|
|
32
|
+
if (args.feature_key && args.feature_key !== activeSession.feature_key) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Active feature lock is '${activeSession.feature_key}' (owner: ${activeSession.owner_id}). Cannot check boundaries for different feature '${args.feature_key}' while another feature is locked.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
effectiveFeatureKey = activeSession.feature_key;
|
|
38
|
+
if (!config.features) config.features = {};
|
|
39
|
+
if (!config.features[activeSession.feature_key]) {
|
|
40
|
+
config.features[activeSession.feature_key] = {
|
|
41
|
+
domain: activeSession.domain,
|
|
42
|
+
description: `Active session: ${activeSession.feature_key}`,
|
|
43
|
+
allowed_touchpoints: activeSession.touchpoints,
|
|
44
|
+
reuse_symbols: [],
|
|
45
|
+
input_contract: {},
|
|
46
|
+
output_contract: {},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const syncState = repo?.getMeta("sync_state");
|
|
52
|
+
const syncError = repo?.getMeta("sync_error");
|
|
53
|
+
if (!syncState || syncState === "running" || syncState === "error" || syncState !== "complete" || syncError) {
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text" as const,
|
|
58
|
+
text: JSON.stringify({
|
|
59
|
+
status: "unknown",
|
|
60
|
+
sync_state: syncState || "unindexed",
|
|
61
|
+
sync_error: syncError || undefined,
|
|
62
|
+
message: "Boundary audit is incomplete because workspace synchronization has not completed.",
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const skipped = JSON.parse(repo?.getMeta("skipped_files") || "[]") as {path: string; reason: string}[];
|
|
23
69
|
const unsupported = JSON.parse(repo?.getMeta("unsupported_files") || "[]") as string[];
|
|
24
70
|
const requested = [...(Array.isArray(args.file_path) ? args.file_path : args.file_path ? [args.file_path] : []), ...(args.file_paths || [])]
|
|
25
71
|
.map(file => relative(root, resolve(root, file)).replace(/\\/g, "/"));
|
|
@@ -27,6 +73,13 @@ export async function handleCheckBoundary(
|
|
|
27
73
|
(!args.domain || evaluator.resolveSourceDomain(file, config) === args.domain) &&
|
|
28
74
|
(requested.length === 0 || requested.includes(file))
|
|
29
75
|
);
|
|
76
|
+
const relevantSkipped = skipped.filter(file =>
|
|
77
|
+
(!args.domain || evaluator.resolveSourceDomain(file.path, config) === args.domain) &&
|
|
78
|
+
(requested.length === 0 || requested.includes(file.path))
|
|
79
|
+
);
|
|
80
|
+
if (relevantSkipped.length > 0 && args.scope !== "staged" && !args.staged) {
|
|
81
|
+
return { content: [{type: "text" as const, text: JSON.stringify({status: "unknown", skipped_files: relevantSkipped, message: "Boundary audit is incomplete because source files exceed resource limits."})}] };
|
|
82
|
+
}
|
|
30
83
|
if (relevantUnsupported.length > 0 && args.scope !== "staged" && !args.staged) {
|
|
31
84
|
return { content: [{ type: "text" as const, text: JSON.stringify({ status: "unknown", unsupported_files: relevantUnsupported, message: "Boundary audit is incomplete because source files have no supported parser." }) }] };
|
|
32
85
|
}
|
|
@@ -48,9 +101,9 @@ export async function handleCheckBoundary(
|
|
|
48
101
|
args.scope ?? (args.staged ? "staged" : args.domain ? "domain" : "all");
|
|
49
102
|
|
|
50
103
|
if (effectiveScope === "staged") {
|
|
51
|
-
const { files: stagedFiles, violations: stagedViolations } = await auditStagedFiles(evaluator, config, root,
|
|
104
|
+
const { files: stagedFiles, violations: stagedViolations } = await auditStagedFiles(evaluator, config, root, effectiveFeatureKey, repo);
|
|
52
105
|
|
|
53
|
-
if (stagedFiles.length === 0) {
|
|
106
|
+
if (stagedFiles.length === 0 && stagedViolations.length === 0) {
|
|
54
107
|
return {
|
|
55
108
|
content: [
|
|
56
109
|
{
|
|
@@ -117,6 +170,25 @@ export async function handleCheckBoundary(
|
|
|
117
170
|
}
|
|
118
171
|
|
|
119
172
|
// Scope: "all" or "domain"
|
|
173
|
+
if (args.feature_key) {
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: "text" as const,
|
|
178
|
+
text: JSON.stringify(
|
|
179
|
+
{
|
|
180
|
+
status: "rejected",
|
|
181
|
+
scope: effectiveScope,
|
|
182
|
+
message: `Scope '${effectiveScope}' is an architectural audit across domains and does not support feature touchpoint validation ('feature_key'). Feature scopes only apply to 'file', 'batch', or 'staged'.`,
|
|
183
|
+
},
|
|
184
|
+
null,
|
|
185
|
+
2
|
|
186
|
+
),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
120
192
|
let violations = evaluator.auditCodebase(config);
|
|
121
193
|
if (args.domain) {
|
|
122
194
|
const targetDom = args.domain.toLowerCase();
|
|
@@ -157,7 +229,7 @@ export async function handleCheckBoundary(
|
|
|
157
229
|
args.domain ? ` in domain '${args.domain}'` : " across codebase"
|
|
158
230
|
}.`
|
|
159
231
|
: domainCount === 0
|
|
160
|
-
? "No boundary violations detected. [Septum Notice: Domain catalog is currently unindexed. Run '
|
|
232
|
+
? "No boundary violations detected. [Septum Notice: Domain catalog is currently unindexed. Run 'septum_domain' with action='register' to initialize bounded contexts.]"
|
|
161
233
|
: `All domain boundaries are clean${
|
|
162
234
|
args.domain ? ` for domain '${args.domain}'` : ""
|
|
163
235
|
}. Zero cross-domain leaks or circular dependencies detected.`,
|
|
@@ -173,7 +245,7 @@ export async function handleCheckBoundary(
|
|
|
173
245
|
// Multi-file batch evaluation
|
|
174
246
|
if (filesToCheck.length > 1) {
|
|
175
247
|
const results = filesToCheck.map((filePath) => {
|
|
176
|
-
const violations = evaluator.evaluate(filePath, proposedImports,
|
|
248
|
+
const violations = evaluator.evaluate(filePath, proposedImports, effectiveFeatureKey, config);
|
|
177
249
|
const impact = repo?.getFileInboundImpact(filePath);
|
|
178
250
|
return {
|
|
179
251
|
file: filePath,
|
|
@@ -206,7 +278,7 @@ export async function handleCheckBoundary(
|
|
|
206
278
|
message: hasViolations
|
|
207
279
|
? `${totalViolations} boundary violation(s) detected across ${filesToCheck.length} file(s).`
|
|
208
280
|
: Object.keys(config?.domains || {}).length === 0
|
|
209
|
-
? `All ${filesToCheck.length} file(s) approved. [Septum Notice: Domain catalog is currently unindexed. Run '
|
|
281
|
+
? `All ${filesToCheck.length} file(s) approved. [Septum Notice: Domain catalog is currently unindexed. Run 'septum_domain' with action='register' to initialize bounded contexts.]`
|
|
210
282
|
: `All ${filesToCheck.length} file(s) approved. No boundary violations detected.`,
|
|
211
283
|
},
|
|
212
284
|
null,
|
|
@@ -219,7 +291,7 @@ export async function handleCheckBoundary(
|
|
|
219
291
|
|
|
220
292
|
// Single file evaluation (backward-compatible output)
|
|
221
293
|
const singleFile = filesToCheck[0];
|
|
222
|
-
const violations = evaluator.evaluate(singleFile, proposedImports,
|
|
294
|
+
const violations = evaluator.evaluate(singleFile, proposedImports, effectiveFeatureKey, config);
|
|
223
295
|
const impact = repo?.getFileInboundImpact(singleFile);
|
|
224
296
|
|
|
225
297
|
if (violations.length > 0) {
|
|
@@ -243,7 +315,7 @@ export async function handleCheckBoundary(
|
|
|
243
315
|
: undefined,
|
|
244
316
|
message:
|
|
245
317
|
Object.keys(config?.domains || {}).length === 0
|
|
246
|
-
? "Boundary integrity check failed: Domain catalog is currently unindexed. Run '
|
|
318
|
+
? "Boundary integrity check failed: Domain catalog is currently unindexed. Run 'septum_domain' with action='register' to initialize bounded contexts."
|
|
247
319
|
: violations.some((v) => v.rule === "unmapped_domain")
|
|
248
320
|
? "Boundary integrity check failed: File does not belong to any declared bounded context."
|
|
249
321
|
: "Boundary violation detected. Proposed imports violate bounded context architecture.",
|
|
@@ -274,7 +346,7 @@ export async function handleCheckBoundary(
|
|
|
274
346
|
: undefined,
|
|
275
347
|
message:
|
|
276
348
|
Object.keys(config?.domains || {}).length === 0
|
|
277
|
-
? "No boundary violations detected. [Septum Notice: Domain catalog is currently unindexed. Run '
|
|
349
|
+
? "No boundary violations detected. [Septum Notice: Domain catalog is currently unindexed. Run 'septum_domain' with action='register' to initialize bounded contexts.]"
|
|
278
350
|
: impact && impact.direct_dependents_count > 0
|
|
279
351
|
? `No boundary violations detected. Caution: ${impact.direct_dependents_count} dependent file(s) consume this file.`
|
|
280
352
|
: "No boundary violations detected. Proposed imports are permissible.",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
2
|
+
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
3
|
+
import type { DomainToolArgs } from "../schemas.ts";
|
|
4
|
+
import { handleGetDomainCatalog } from "./get-domain-catalog.ts";
|
|
5
|
+
import { handleRegisterDomain } from "./register-domain.ts";
|
|
6
|
+
import { handleGetEnvironmentTopology } from "./get-environment-topology.ts";
|
|
7
|
+
|
|
8
|
+
export type { DomainToolArgs };
|
|
9
|
+
|
|
10
|
+
export async function handleDomainTool(
|
|
11
|
+
repo: SeptumRepository,
|
|
12
|
+
config: ValidatedSeptumConfig,
|
|
13
|
+
args: DomainToolArgs,
|
|
14
|
+
workspaceRoot?: string,
|
|
15
|
+
signal?: AbortSignal
|
|
16
|
+
) {
|
|
17
|
+
const action = args.action || "catalog";
|
|
18
|
+
|
|
19
|
+
switch (action) {
|
|
20
|
+
case "register":
|
|
21
|
+
return await handleRegisterDomain(repo, config, args, workspaceRoot, signal);
|
|
22
|
+
|
|
23
|
+
case "topology":
|
|
24
|
+
return handleGetEnvironmentTopology(
|
|
25
|
+
repo,
|
|
26
|
+
config,
|
|
27
|
+
{
|
|
28
|
+
resolve: args.resolve,
|
|
29
|
+
resolveNode: args.resolve_node,
|
|
30
|
+
refresh: args.refresh,
|
|
31
|
+
workspace_path: args.workspace_path,
|
|
32
|
+
},
|
|
33
|
+
workspaceRoot || args.workspace_path || process.cwd()
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
case "catalog":
|
|
37
|
+
default:
|
|
38
|
+
return handleGetDomainCatalog(repo, config, args, workspaceRoot);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
2
|
+
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
3
|
+
import type { FeatureToolArgs } from "../schemas.ts";
|
|
4
|
+
import { handleGetFeatureContext, handleClearFeatureContext } from "./get-feature-context.ts";
|
|
5
|
+
import { handleRegisterFeature } from "./register-feature.ts";
|
|
6
|
+
|
|
7
|
+
export type { FeatureToolArgs };
|
|
8
|
+
|
|
9
|
+
export async function handleFeatureTool(
|
|
10
|
+
repo: SeptumRepository,
|
|
11
|
+
config: ValidatedSeptumConfig,
|
|
12
|
+
args: FeatureToolArgs,
|
|
13
|
+
workspaceRoot?: string
|
|
14
|
+
) {
|
|
15
|
+
const action = args.action || "get";
|
|
16
|
+
|
|
17
|
+
switch (action) {
|
|
18
|
+
case "lock":
|
|
19
|
+
return await handleRegisterFeature(
|
|
20
|
+
repo,
|
|
21
|
+
config,
|
|
22
|
+
{
|
|
23
|
+
feature_key: args.feature_key || args.feature || "",
|
|
24
|
+
feature: args.feature || args.feature_key || "",
|
|
25
|
+
domain: args.domain || args.domain_name || "",
|
|
26
|
+
domain_name: args.domain_name || args.domain || "",
|
|
27
|
+
description: args.description,
|
|
28
|
+
allowed_touchpoints: args.allowed_touchpoints,
|
|
29
|
+
reuse_symbols: args.reuse_symbols,
|
|
30
|
+
input_contract: args.input_contract,
|
|
31
|
+
output_contract: args.output_contract,
|
|
32
|
+
workspace_path: args.workspace_path,
|
|
33
|
+
},
|
|
34
|
+
workspaceRoot || args.workspace_path || process.cwd()
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
case "release": {
|
|
38
|
+
const cleared = handleClearFeatureContext(
|
|
39
|
+
workspaceRoot || args.workspace_path || process.cwd(),
|
|
40
|
+
Boolean(args.force)
|
|
41
|
+
);
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text" as const,
|
|
46
|
+
text: cleared
|
|
47
|
+
? "Active feature session successfully released."
|
|
48
|
+
: "No active feature session found to release.",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
case "get":
|
|
55
|
+
default:
|
|
56
|
+
return handleGetFeatureContext(
|
|
57
|
+
repo,
|
|
58
|
+
config,
|
|
59
|
+
{
|
|
60
|
+
feature_key: args.feature_key || args.feature,
|
|
61
|
+
feature: args.feature || args.feature_key,
|
|
62
|
+
workspace_path: args.workspace_path,
|
|
63
|
+
},
|
|
64
|
+
workspaceRoot || args.workspace_path || process.cwd()
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -18,6 +18,8 @@ export function handleGetDomainCatalog(
|
|
|
18
18
|
const indexStatus = {
|
|
19
19
|
unsupported_files: JSON.parse(repo.getMeta("unsupported_files") || "[]"),
|
|
20
20
|
watcher_mode: repo.getMeta("watcher_mode") || "manual",
|
|
21
|
+
sync_state: repo.getMeta("sync_state") || "unknown",
|
|
22
|
+
skipped_files: JSON.parse(repo.getMeta("skipped_files") || "[]"),
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
if (!domainName) {
|
|
@@ -29,15 +31,16 @@ export function handleGetDomainCatalog(
|
|
|
29
31
|
total_domains: 0,
|
|
30
32
|
message: `Workspace '${root}' belum memiliki domain terdaftar dalam Septum catalog.`,
|
|
31
33
|
suggested_action: {
|
|
32
|
-
tool: "
|
|
34
|
+
tool: "septum_domain",
|
|
33
35
|
params: {
|
|
36
|
+
action: "register",
|
|
34
37
|
name: "core",
|
|
35
38
|
root: "src",
|
|
36
39
|
ingest_now: true,
|
|
37
40
|
},
|
|
38
41
|
},
|
|
39
42
|
agent_guidance:
|
|
40
|
-
"Panggil tool '
|
|
43
|
+
"Panggil tool 'septum_domain(action: \"register\", name: \"core\", root: \"src\", ingest_now: true)' untuk mendaftarkan dan mengindeks domain utama secara otomatis, atau jalankan 'septum init' di terminal workspace.",
|
|
41
44
|
};
|
|
42
45
|
return {
|
|
43
46
|
content: [
|
|
@@ -75,7 +78,7 @@ export function handleGetDomainCatalog(
|
|
|
75
78
|
forbidden_dependencies: JSON.parse(d.forbidden_deps_json || "[]"),
|
|
76
79
|
})),
|
|
77
80
|
agent_instruction:
|
|
78
|
-
"Macro semantic map loaded from SQLite SSOT. Use '
|
|
81
|
+
"Macro semantic map loaded from SQLite SSOT. Use 'septum_domain' with domain='<name>' for granular file/symbol catalogs, or 'septum_slice' with query='<route|command|intent>' for end-to-end execution flow.",
|
|
79
82
|
};
|
|
80
83
|
|
|
81
84
|
return {
|
|
@@ -111,8 +114,8 @@ export function handleGetDomainCatalog(
|
|
|
111
114
|
available_domains: available,
|
|
112
115
|
suggestions: suggestions.length > 0 ? suggestions : available,
|
|
113
116
|
suggested_action: {
|
|
114
|
-
tool: "
|
|
115
|
-
params: { domain: domainName, root: `src/domains/${domainName}` },
|
|
117
|
+
tool: "septum_domain",
|
|
118
|
+
params: { action: "register", domain: domainName, root: `src/domains/${domainName}` },
|
|
116
119
|
},
|
|
117
120
|
};
|
|
118
121
|
return {
|
|
@@ -144,7 +147,7 @@ function recordCatalogAccess(domain: string, projectRoot: string = process.cwd()
|
|
|
144
147
|
try {
|
|
145
148
|
const septumDir = path.resolve(projectRoot, ".septum");
|
|
146
149
|
if (!fs.existsSync(septumDir)) {
|
|
147
|
-
|
|
150
|
+
return;
|
|
148
151
|
}
|
|
149
152
|
const logPath = path.join(septumDir, "catalog_access.json");
|
|
150
153
|
let log: { domains?: Record<string, string>; features?: Record<string, string> } = {};
|
|
@@ -122,9 +122,9 @@ export function handleGetEnvironmentTopology(
|
|
|
122
122
|
output += `- ${q}\n`;
|
|
123
123
|
}
|
|
124
124
|
output += `\n*Untuk mengunci jawaban pengguna ke SQLite SSOT, panggil kembali tool ini dengan argumen:*\n`;
|
|
125
|
-
output += `\`
|
|
125
|
+
output += `\`septum_domain(action: "topology", resolve: { layer: "<layer_name>", platform: "<platform_name>" })\`\n`;
|
|
126
126
|
output += `*atau untuk entitas terdistribusi:*\n`;
|
|
127
|
-
output += `\`
|
|
127
|
+
output += `\`septum_domain(action: "topology", resolve_node: { id: "<node_id>", layer: "<layer>", name: "<name>", platform: "<platform>", domain_or_ip: "<domain_or_ip>", role: "<role>" })\`\n`;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
return {
|
|
@@ -8,8 +8,11 @@ import type { GetFeatureContextArgs } from "../schemas.ts";
|
|
|
8
8
|
|
|
9
9
|
export type { GetFeatureContextArgs } from "../schemas.ts";
|
|
10
10
|
|
|
11
|
-
export function handleClearFeatureContext(
|
|
12
|
-
|
|
11
|
+
export function handleClearFeatureContext(
|
|
12
|
+
workspaceRoot: string = process.cwd(),
|
|
13
|
+
force: boolean = false
|
|
14
|
+
): boolean {
|
|
15
|
+
return SessionManager.clearActiveSession(workspaceRoot, undefined, force);
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
export function handleGetFeatureContext(
|
|
@@ -41,7 +44,7 @@ export function handleGetFeatureContext(
|
|
|
41
44
|
status: "prompt",
|
|
42
45
|
message: "No feature specified. Please specify 'feature_key' from the registered features below or trace routes.",
|
|
43
46
|
available_features: available.length > 0 ? available : [],
|
|
44
|
-
instructions: "Call '
|
|
47
|
+
instructions: "Call 'septum_feature({ action: \"get\", feature_key: \"<name>\" })' or lock a feature with 'septum_feature({ action: \"lock\", feature_key: \"<name>\", domain: \"<domain>\" })'.",
|
|
45
48
|
},
|
|
46
49
|
null,
|
|
47
50
|
2
|
|
@@ -127,8 +130,8 @@ export function handleGetFeatureContext(
|
|
|
127
130
|
feature_key: featureKey,
|
|
128
131
|
message: `Feature '${featureKey}' not found in configuration or vertical slices. Available features: [${availableStr}].`,
|
|
129
132
|
recovery_options: [
|
|
130
|
-
`
|
|
131
|
-
`
|
|
133
|
+
`septum_feature({ action: "lock", feature_key: "${featureKey}", domain: "<domain_name>", allowed_touchpoints: ["path/to/file"] })`,
|
|
134
|
+
`septum_slice({ query: "routes/web.php" })`,
|
|
132
135
|
],
|
|
133
136
|
},
|
|
134
137
|
null,
|
|
@@ -140,13 +143,7 @@ export function handleGetFeatureContext(
|
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
//
|
|
144
|
-
SessionManager.saveActiveSession(workspaceRoot, {
|
|
145
|
-
feature_key: featureKey,
|
|
146
|
-
domain: featureConfig.domain,
|
|
147
|
-
touchpoints: featureConfig.allowed_touchpoints ?? [],
|
|
148
|
-
locked_at: new Date().toISOString(),
|
|
149
|
-
});
|
|
146
|
+
// MCP-14: handleGetFeatureContext is strictly read-only and does not mutate session leases
|
|
150
147
|
|
|
151
148
|
// Deterministically fetch signatures, methods, and properties for reuse_symbols from SQLite
|
|
152
149
|
const reuseSymbolsWithMeta: FeatureContextResponse["reuse_symbols"] = [];
|
|
@@ -6,7 +6,7 @@ export function handleGetSymbolImpact(
|
|
|
6
6
|
repo: SeptumRepository,
|
|
7
7
|
_config: ValidatedSeptumConfig,
|
|
8
8
|
args: GetSymbolImpactArgs,
|
|
9
|
-
|
|
9
|
+
workspaceRoot?: string
|
|
10
10
|
) {
|
|
11
11
|
const symbol = (args.symbol || (args as unknown as Record<string, unknown>).query || "").toString().trim();
|
|
12
12
|
if (!symbol) {
|
|
@@ -15,7 +15,10 @@ export function handleGetSymbolImpact(
|
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const impact = repo.getSymbolImpact(symbol
|
|
18
|
+
const impact = repo.getSymbolImpact(symbol, {
|
|
19
|
+
domain: args.domain,
|
|
20
|
+
workspaceRoot: workspaceRoot || args.workspace_path,
|
|
21
|
+
});
|
|
19
22
|
|
|
20
23
|
return {
|
|
21
24
|
content: [
|
|
@@ -7,7 +7,7 @@ export function handleGetSymbol(
|
|
|
7
7
|
repo: SeptumRepository,
|
|
8
8
|
_config: ValidatedSeptumConfig,
|
|
9
9
|
args: GetSymbolArgs,
|
|
10
|
-
|
|
10
|
+
workspaceRoot?: string
|
|
11
11
|
) {
|
|
12
12
|
const symbol = (args.symbol || (args as unknown as Record<string, unknown>).query || "").toString().trim();
|
|
13
13
|
if (!symbol) {
|
|
@@ -19,6 +19,7 @@ export function handleGetSymbol(
|
|
|
19
19
|
const details = repo.getSymbolDetails(symbol, {
|
|
20
20
|
domain: args.domain,
|
|
21
21
|
includeDependencies: args.include_dependencies,
|
|
22
|
+
workspaceRoot: workspaceRoot || args.workspace_path,
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
if (details) {
|
|
@@ -43,11 +43,11 @@ export async function handleLocateSymbol(
|
|
|
43
43
|
query,
|
|
44
44
|
message: `Workspace '${root}' belum terindeks dalam Septum catalog.`,
|
|
45
45
|
suggested_action: {
|
|
46
|
-
tool: "
|
|
47
|
-
params: { name: "core", root: "src", ingest_now: true },
|
|
46
|
+
tool: "septum_domain",
|
|
47
|
+
params: { action: "register", name: "core", root: "src", ingest_now: true },
|
|
48
48
|
},
|
|
49
49
|
agent_guidance:
|
|
50
|
-
"Panggil tool '
|
|
50
|
+
"Panggil tool 'septum_domain(action: \"register\", name: \"core\", root: \"src\", ingest_now: true)' untuk mendaftarkan dan mengindeks codebase ini, atau jalankan 'septum init' di terminal workspace.",
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
2
2
|
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
3
3
|
import { IngestionPipeline, type IngestionMetrics } from "../../core/ingestion/pipeline.ts";
|
|
4
|
+
import { assertPathContained } from "../../core/resolver/path-resolver.ts";
|
|
4
5
|
import type { RegisterDomainArgs } from "../schemas.ts";
|
|
5
6
|
|
|
6
7
|
export type { RegisterDomainArgs } from "../schemas.ts";
|
|
@@ -9,24 +10,39 @@ export async function handleRegisterDomain(
|
|
|
9
10
|
repo: SeptumRepository,
|
|
10
11
|
config: ValidatedSeptumConfig,
|
|
11
12
|
args: Partial<RegisterDomainArgs>,
|
|
12
|
-
workspaceRoot: string = process.cwd()
|
|
13
|
+
workspaceRoot: string = process.cwd(),
|
|
14
|
+
signal?: AbortSignal
|
|
13
15
|
) {
|
|
16
|
+
signal?.throwIfAborted();
|
|
14
17
|
const domainName = (args.domain || args.name || "").trim();
|
|
15
18
|
if (!domainName || !args.root) {
|
|
16
19
|
throw new Error("Missing required arguments: 'domain' (or 'name') and 'root' are required.");
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
// SV-02: Enforce workspace containment (reject traversal/absolute/symlink escape)
|
|
23
|
+
assertPathContained(args.root, workspaceRoot, "Domain root");
|
|
24
|
+
signal?.throwIfAborted();
|
|
25
|
+
|
|
19
26
|
const domainConfig = {
|
|
20
27
|
root: args.root,
|
|
21
28
|
description: args.description || `Domain: ${domainName}`,
|
|
22
|
-
allowed_dependencies: args.allowed_dependencies
|
|
23
|
-
forbidden_dependencies: args.forbidden_dependencies
|
|
24
|
-
archetypes: args.archetypes
|
|
29
|
+
allowed_dependencies: args.allowed_dependencies,
|
|
30
|
+
forbidden_dependencies: args.forbidden_dependencies,
|
|
31
|
+
archetypes: args.archetypes,
|
|
25
32
|
};
|
|
26
33
|
|
|
27
34
|
const domainId = repo.domains.upsertDomain(domainName, domainConfig);
|
|
35
|
+
const stored = repo.domains.getDomainByName(domainName);
|
|
36
|
+
const canonicalName = stored?.name || domainName;
|
|
37
|
+
const finalConfig = {
|
|
38
|
+
root: stored?.root_path || domainConfig.root,
|
|
39
|
+
description: domainConfig.description,
|
|
40
|
+
allowed_dependencies: stored ? JSON.parse(stored.allowed_deps_json || "[]") : (domainConfig.allowed_dependencies || []),
|
|
41
|
+
forbidden_dependencies: stored ? JSON.parse(stored.forbidden_deps_json || "[]") : (domainConfig.forbidden_dependencies || []),
|
|
42
|
+
archetypes: stored ? JSON.parse(stored.archetypes_json || "{}") : (domainConfig.archetypes || {}),
|
|
43
|
+
};
|
|
28
44
|
|
|
29
|
-
config.domains[
|
|
45
|
+
config.domains[canonicalName] = finalConfig;
|
|
30
46
|
|
|
31
47
|
let ingestionMetrics: IngestionMetrics | null = null;
|
|
32
48
|
if (args.ingest_now) {
|
|
@@ -41,13 +57,13 @@ export async function handleRegisterDomain(
|
|
|
41
57
|
text: JSON.stringify(
|
|
42
58
|
{
|
|
43
59
|
status: "success",
|
|
44
|
-
message: `Domain '${
|
|
60
|
+
message: `Domain '${canonicalName}' successfully registered in SQLite SSOT.${
|
|
45
61
|
args.ingest_now ? " Initial ingestion completed." : ""
|
|
46
62
|
}`,
|
|
47
63
|
domain_id: domainId,
|
|
48
|
-
domain:
|
|
49
|
-
name:
|
|
50
|
-
config:
|
|
64
|
+
domain: canonicalName,
|
|
65
|
+
name: canonicalName,
|
|
66
|
+
config: finalConfig,
|
|
51
67
|
...(ingestionMetrics ? { ingestion_metrics: ingestionMetrics } : {}),
|
|
52
68
|
},
|
|
53
69
|
null,
|
|
@@ -18,6 +18,16 @@ export async function handleRegisterFeature(
|
|
|
18
18
|
throw new Error("Missing required arguments: 'feature_key' and 'domain' are required.");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// MCP-15: Validate domain is registered in DB or config
|
|
22
|
+
const registeredDomain = repo.getDomainByName(domain);
|
|
23
|
+
const configDomain = Object.keys(config.domains || {}).find(
|
|
24
|
+
(d) => d.toLowerCase() === domain.toLowerCase()
|
|
25
|
+
);
|
|
26
|
+
if (!registeredDomain && !configDomain) {
|
|
27
|
+
throw new Error(`Domain '${domain}' is not registered. Cannot lock feature to an unknown domain.`);
|
|
28
|
+
}
|
|
29
|
+
const canonicalDomain = registeredDomain?.name || configDomain || domain;
|
|
30
|
+
|
|
21
31
|
const rawTouchpoints = args.allowed_touchpoints ?? [];
|
|
22
32
|
const allowedTouchpoints = Array.isArray(rawTouchpoints)
|
|
23
33
|
? rawTouchpoints
|
|
@@ -25,6 +35,18 @@ export async function handleRegisterFeature(
|
|
|
25
35
|
? [rawTouchpoints]
|
|
26
36
|
: [];
|
|
27
37
|
|
|
38
|
+
// MCP-15: Validate supported touchpoint glob patterns (exact, /*, /**)
|
|
39
|
+
for (const tp of allowedTouchpoints) {
|
|
40
|
+
const normalized = tp.replace(/\\/g, "/");
|
|
41
|
+
if (normalized.includes("*")) {
|
|
42
|
+
if (!normalized.endsWith("/*") && !normalized.endsWith("/**") && normalized !== "*" && normalized !== "**") {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Unsupported touchpoint glob pattern '${tp}'. Septum touchpoints support exact paths, '/*', and '/**'.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
const rawReuse = args.reuse_symbols;
|
|
29
51
|
const reuseSymbols: string[] = Array.isArray(rawReuse)
|
|
30
52
|
? rawReuse
|
|
@@ -33,7 +55,7 @@ export async function handleRegisterFeature(
|
|
|
33
55
|
: [];
|
|
34
56
|
|
|
35
57
|
const featureConfig = {
|
|
36
|
-
domain,
|
|
58
|
+
domain: canonicalDomain,
|
|
37
59
|
description: args.description || `Feature: ${featureKey}`,
|
|
38
60
|
allowed_touchpoints: allowedTouchpoints,
|
|
39
61
|
reuse_symbols: reuseSymbols,
|
|
@@ -41,32 +63,27 @@ export async function handleRegisterFeature(
|
|
|
41
63
|
output_contract: args.output_contract || {},
|
|
42
64
|
};
|
|
43
65
|
|
|
44
|
-
// Record Active Feature Session Lease (.septum/session.json)
|
|
45
66
|
SessionManager.saveActiveSession(workspaceRoot, {
|
|
46
67
|
feature_key: featureKey,
|
|
47
|
-
domain,
|
|
68
|
+
domain: canonicalDomain,
|
|
48
69
|
touchpoints: featureConfig.allowed_touchpoints,
|
|
49
70
|
locked_at: new Date().toISOString(),
|
|
71
|
+
}, SessionManager.getOwnerId(), {
|
|
72
|
+
repo,
|
|
73
|
+
save: () => {
|
|
74
|
+
const existingRaw = repo.getMeta("features_json");
|
|
75
|
+
const featuresMap = existingRaw ? JSON.parse(existingRaw) : {};
|
|
76
|
+
featuresMap[featureKey] = featureConfig;
|
|
77
|
+
repo.setMeta("features_json", JSON.stringify(featuresMap));
|
|
78
|
+
},
|
|
50
79
|
});
|
|
51
80
|
|
|
81
|
+
// 3. Update in-memory config only after both SQLite and session lease succeeded
|
|
52
82
|
if (!config.features) {
|
|
53
83
|
config.features = {};
|
|
54
84
|
}
|
|
55
85
|
config.features[featureKey] = featureConfig;
|
|
56
86
|
|
|
57
|
-
if (typeof (repo as any)?.upsertFeature === "function") {
|
|
58
|
-
(repo as any).upsertFeature(featureKey, featureConfig);
|
|
59
|
-
} else if (typeof repo?.setMeta === "function") {
|
|
60
|
-
try {
|
|
61
|
-
const existingRaw = repo.getMeta("features_json");
|
|
62
|
-
const featuresMap = existingRaw ? JSON.parse(existingRaw) : {};
|
|
63
|
-
featuresMap[featureKey] = featureConfig;
|
|
64
|
-
repo.setMeta("features_json", JSON.stringify(featuresMap));
|
|
65
|
-
} catch {
|
|
66
|
-
// Ignore meta persistence errors on mock repositories
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
87
|
|
|
71
88
|
|
|
72
89
|
return {
|
|
@@ -79,7 +96,7 @@ export async function handleRegisterFeature(
|
|
|
79
96
|
message: `Feature '${featureKey}' successfully registered in SQLite SSOT and session lease established.`,
|
|
80
97
|
feature_key: featureKey,
|
|
81
98
|
feature: featureKey,
|
|
82
|
-
domain,
|
|
99
|
+
domain: canonicalDomain,
|
|
83
100
|
touchpoints: featureConfig.allowed_touchpoints,
|
|
84
101
|
reuse_symbols: featureConfig.reuse_symbols,
|
|
85
102
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
2
|
+
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
3
|
+
import type { SliceToolArgs } from "../schemas.ts";
|
|
4
|
+
import { handleTraceVerticalSlice } from "./trace-vertical-slice.ts";
|
|
5
|
+
|
|
6
|
+
export type { SliceToolArgs };
|
|
7
|
+
|
|
8
|
+
export function handleSliceTool(
|
|
9
|
+
repo: SeptumRepository,
|
|
10
|
+
config: ValidatedSeptumConfig,
|
|
11
|
+
args: SliceToolArgs,
|
|
12
|
+
workspaceRoot?: string
|
|
13
|
+
) {
|
|
14
|
+
return handleTraceVerticalSlice(repo, config, args, workspaceRoot);
|
|
15
|
+
}
|