septum 0.1.6 → 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 +38 -37
- package/package.json +6 -5
- package/src/cli/commands/check.ts +34 -122
- package/src/cli/commands/hook.ts +68 -16
- 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 +7 -68
- 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/evaluator.ts +21 -25
- package/src/core/boundary/staged-auditor.ts +50 -0
- package/src/core/config/loader.ts +21 -24
- package/src/core/database/client.ts +8 -0
- package/src/core/database/repositories/dependency.repository.ts +80 -16
- package/src/core/database/repositories/domain.repository.ts +86 -33
- package/src/core/database/repositories/symbol.repository.ts +147 -65
- 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 +274 -126
- package/src/core/parser/extractors/frontend.ts +1 -0
- package/src/core/parser/extractors/go.ts +2 -2
- 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/extractors/php.ts +2 -2
- package/src/core/parser/extractors/python.ts +2 -2
- package/src/core/parser/extractors/typescript.ts +2 -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/parser/tree-sitter.ts +6 -1
- package/src/core/parser/wasm-assets.ts +17 -0
- package/src/core/parser/wasm-loader.ts +3 -0
- package/src/core/resolver/call-graph-tracer.ts +4 -3
- package/src/core/resolver/module-resolver.ts +1 -10
- package/src/core/resolver/path-resolver.ts +57 -4
- package/src/core/resolver/vertical-slice-tracer.ts +1 -0
- 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 +67 -38
- 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 +326 -529
- package/src/mcp/tools/boundary.ts +17 -0
- package/src/mcp/tools/check-boundary.ts +97 -43
- package/src/mcp/tools/domain.ts +40 -0
- package/src/mcp/tools/feature.ts +67 -0
- package/src/mcp/tools/get-domain-catalog.ts +15 -7
- 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 +28 -5
- package/src/mcp/tools/register-domain.ts +25 -9
- package/src/mcp/tools/register-feature.ts +40 -21
- 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 +21 -1
- package/src/types/wasm.d.ts +4 -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
|
+
}
|
|
@@ -1,23 +1,88 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { relative, resolve } from "node:path";
|
|
2
|
+
import { auditStagedFiles } from "../../core/boundary/staged-auditor.ts";
|
|
2
3
|
import type { BoundaryEvaluator } from "../../core/boundary/evaluator.ts";
|
|
3
4
|
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
4
5
|
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
6
|
+
import { SessionManager } from "../../core/session/session-manager.ts";
|
|
5
7
|
import type { BoundaryViolation } from "../../types/index.ts";
|
|
6
|
-
import {
|
|
8
|
+
import { CheckBoundarySchema, type CheckBoundaryArgs } from "../schemas.ts";
|
|
7
9
|
|
|
8
10
|
export type { CheckBoundaryArgs } from "../schemas.ts";
|
|
9
11
|
|
|
10
|
-
export function handleCheckBoundary(
|
|
12
|
+
export async function handleCheckBoundary(
|
|
11
13
|
evaluator: BoundaryEvaluator,
|
|
12
14
|
config: ValidatedSeptumConfig,
|
|
13
15
|
rawArgs: CheckBoundaryArgs & Record<string, unknown>,
|
|
14
16
|
repo?: SeptumRepository,
|
|
15
17
|
workspaceRoot?: string
|
|
16
18
|
) {
|
|
17
|
-
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
|
+
}
|
|
18
25
|
const root = workspaceRoot || args.workspace_path || process.cwd();
|
|
19
26
|
const proposedImports = args.proposed_imports ?? [];
|
|
20
|
-
|
|
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}[];
|
|
69
|
+
const unsupported = JSON.parse(repo?.getMeta("unsupported_files") || "[]") as string[];
|
|
70
|
+
const requested = [...(Array.isArray(args.file_path) ? args.file_path : args.file_path ? [args.file_path] : []), ...(args.file_paths || [])]
|
|
71
|
+
.map(file => relative(root, resolve(root, file)).replace(/\\/g, "/"));
|
|
72
|
+
const relevantUnsupported = unsupported.filter(file =>
|
|
73
|
+
(!args.domain || evaluator.resolveSourceDomain(file, config) === args.domain) &&
|
|
74
|
+
(requested.length === 0 || requested.includes(file))
|
|
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
|
+
}
|
|
83
|
+
if (relevantUnsupported.length > 0 && args.scope !== "staged" && !args.staged) {
|
|
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." }) }] };
|
|
85
|
+
}
|
|
21
86
|
|
|
22
87
|
const filesToCheck: string[] = [];
|
|
23
88
|
if (args.file_paths && Array.isArray(args.file_paths) && args.file_paths.length > 0) {
|
|
@@ -36,22 +101,9 @@ export function handleCheckBoundary(
|
|
|
36
101
|
args.scope ?? (args.staged ? "staged" : args.domain ? "domain" : "all");
|
|
37
102
|
|
|
38
103
|
if (effectiveScope === "staged") {
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const gitOut = execSync("git diff --name-only --cached", {
|
|
42
|
-
cwd: root,
|
|
43
|
-
encoding: "utf-8",
|
|
44
|
-
timeout: 5000,
|
|
45
|
-
});
|
|
46
|
-
stagedFiles = gitOut
|
|
47
|
-
.split("\n")
|
|
48
|
-
.map((f) => f.trim())
|
|
49
|
-
.filter(Boolean);
|
|
50
|
-
} catch {
|
|
51
|
-
stagedFiles = [];
|
|
52
|
-
}
|
|
104
|
+
const { files: stagedFiles, violations: stagedViolations } = await auditStagedFiles(evaluator, config, root, effectiveFeatureKey, repo);
|
|
53
105
|
|
|
54
|
-
if (stagedFiles.length === 0) {
|
|
106
|
+
if (stagedFiles.length === 0 && stagedViolations.length === 0) {
|
|
55
107
|
return {
|
|
56
108
|
content: [
|
|
57
109
|
{
|
|
@@ -75,23 +127,6 @@ export function handleCheckBoundary(
|
|
|
75
127
|
};
|
|
76
128
|
}
|
|
77
129
|
|
|
78
|
-
const codebaseViolations = evaluator.auditCodebase(config);
|
|
79
|
-
const stagedNormalized = new Set(
|
|
80
|
-
stagedFiles.map((f) => f.replace(/\\/g, "/").replace(/^\.\//, ""))
|
|
81
|
-
);
|
|
82
|
-
const stagedViolations: BoundaryViolation[] = [];
|
|
83
|
-
|
|
84
|
-
for (const filePath of stagedFiles) {
|
|
85
|
-
const evalViolations = evaluator.evaluate(filePath, proposedImports, featureKey, config);
|
|
86
|
-
stagedViolations.push(...evalViolations);
|
|
87
|
-
}
|
|
88
|
-
for (const v of codebaseViolations) {
|
|
89
|
-
const normFile = v.file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
90
|
-
if (stagedNormalized.has(normFile)) {
|
|
91
|
-
stagedViolations.push(v);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
130
|
const filteredViolations = args.domain
|
|
96
131
|
? stagedViolations.filter(
|
|
97
132
|
(v) =>
|
|
@@ -135,6 +170,25 @@ export function handleCheckBoundary(
|
|
|
135
170
|
}
|
|
136
171
|
|
|
137
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
|
+
|
|
138
192
|
let violations = evaluator.auditCodebase(config);
|
|
139
193
|
if (args.domain) {
|
|
140
194
|
const targetDom = args.domain.toLowerCase();
|
|
@@ -175,7 +229,7 @@ export function handleCheckBoundary(
|
|
|
175
229
|
args.domain ? ` in domain '${args.domain}'` : " across codebase"
|
|
176
230
|
}.`
|
|
177
231
|
: domainCount === 0
|
|
178
|
-
? "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.]"
|
|
179
233
|
: `All domain boundaries are clean${
|
|
180
234
|
args.domain ? ` for domain '${args.domain}'` : ""
|
|
181
235
|
}. Zero cross-domain leaks or circular dependencies detected.`,
|
|
@@ -191,7 +245,7 @@ export function handleCheckBoundary(
|
|
|
191
245
|
// Multi-file batch evaluation
|
|
192
246
|
if (filesToCheck.length > 1) {
|
|
193
247
|
const results = filesToCheck.map((filePath) => {
|
|
194
|
-
const violations = evaluator.evaluate(filePath, proposedImports,
|
|
248
|
+
const violations = evaluator.evaluate(filePath, proposedImports, effectiveFeatureKey, config);
|
|
195
249
|
const impact = repo?.getFileInboundImpact(filePath);
|
|
196
250
|
return {
|
|
197
251
|
file: filePath,
|
|
@@ -224,7 +278,7 @@ export function handleCheckBoundary(
|
|
|
224
278
|
message: hasViolations
|
|
225
279
|
? `${totalViolations} boundary violation(s) detected across ${filesToCheck.length} file(s).`
|
|
226
280
|
: Object.keys(config?.domains || {}).length === 0
|
|
227
|
-
? `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.]`
|
|
228
282
|
: `All ${filesToCheck.length} file(s) approved. No boundary violations detected.`,
|
|
229
283
|
},
|
|
230
284
|
null,
|
|
@@ -237,7 +291,7 @@ export function handleCheckBoundary(
|
|
|
237
291
|
|
|
238
292
|
// Single file evaluation (backward-compatible output)
|
|
239
293
|
const singleFile = filesToCheck[0];
|
|
240
|
-
const violations = evaluator.evaluate(singleFile, proposedImports,
|
|
294
|
+
const violations = evaluator.evaluate(singleFile, proposedImports, effectiveFeatureKey, config);
|
|
241
295
|
const impact = repo?.getFileInboundImpact(singleFile);
|
|
242
296
|
|
|
243
297
|
if (violations.length > 0) {
|
|
@@ -261,7 +315,7 @@ export function handleCheckBoundary(
|
|
|
261
315
|
: undefined,
|
|
262
316
|
message:
|
|
263
317
|
Object.keys(config?.domains || {}).length === 0
|
|
264
|
-
? "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."
|
|
265
319
|
: violations.some((v) => v.rule === "unmapped_domain")
|
|
266
320
|
? "Boundary integrity check failed: File does not belong to any declared bounded context."
|
|
267
321
|
: "Boundary violation detected. Proposed imports violate bounded context architecture.",
|
|
@@ -292,7 +346,7 @@ export function handleCheckBoundary(
|
|
|
292
346
|
: undefined,
|
|
293
347
|
message:
|
|
294
348
|
Object.keys(config?.domains || {}).length === 0
|
|
295
|
-
? "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.]"
|
|
296
350
|
: impact && impact.direct_dependents_count > 0
|
|
297
351
|
? `No boundary violations detected. Caution: ${impact.direct_dependents_count} dependent file(s) consume this file.`
|
|
298
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
|
+
}
|
|
@@ -15,6 +15,12 @@ export function handleGetDomainCatalog(
|
|
|
15
15
|
) {
|
|
16
16
|
const targetRoot = args.workspace_path || workspaceRoot || (repo as any)?.projectRoot || process.cwd();
|
|
17
17
|
const domainName = args.domain ?? args.name;
|
|
18
|
+
const indexStatus = {
|
|
19
|
+
unsupported_files: JSON.parse(repo.getMeta("unsupported_files") || "[]"),
|
|
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") || "[]"),
|
|
23
|
+
};
|
|
18
24
|
|
|
19
25
|
if (!domainName) {
|
|
20
26
|
const allDomains = repo.getAllDomains();
|
|
@@ -25,15 +31,16 @@ export function handleGetDomainCatalog(
|
|
|
25
31
|
total_domains: 0,
|
|
26
32
|
message: `Workspace '${root}' belum memiliki domain terdaftar dalam Septum catalog.`,
|
|
27
33
|
suggested_action: {
|
|
28
|
-
tool: "
|
|
34
|
+
tool: "septum_domain",
|
|
29
35
|
params: {
|
|
36
|
+
action: "register",
|
|
30
37
|
name: "core",
|
|
31
38
|
root: "src",
|
|
32
39
|
ingest_now: true,
|
|
33
40
|
},
|
|
34
41
|
},
|
|
35
42
|
agent_guidance:
|
|
36
|
-
"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.",
|
|
37
44
|
};
|
|
38
45
|
return {
|
|
39
46
|
content: [
|
|
@@ -55,6 +62,7 @@ export function handleGetDomainCatalog(
|
|
|
55
62
|
: repo.getAllVerticalSlices().length;
|
|
56
63
|
|
|
57
64
|
const macroMap = {
|
|
65
|
+
index_status: indexStatus,
|
|
58
66
|
telescope_view: {
|
|
59
67
|
project_type: projectType,
|
|
60
68
|
framework,
|
|
@@ -70,7 +78,7 @@ export function handleGetDomainCatalog(
|
|
|
70
78
|
forbidden_dependencies: JSON.parse(d.forbidden_deps_json || "[]"),
|
|
71
79
|
})),
|
|
72
80
|
agent_instruction:
|
|
73
|
-
"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.",
|
|
74
82
|
};
|
|
75
83
|
|
|
76
84
|
return {
|
|
@@ -106,8 +114,8 @@ export function handleGetDomainCatalog(
|
|
|
106
114
|
available_domains: available,
|
|
107
115
|
suggestions: suggestions.length > 0 ? suggestions : available,
|
|
108
116
|
suggested_action: {
|
|
109
|
-
tool: "
|
|
110
|
-
params: { domain: domainName, root: `src/domains/${domainName}` },
|
|
117
|
+
tool: "septum_domain",
|
|
118
|
+
params: { action: "register", domain: domainName, root: `src/domains/${domainName}` },
|
|
111
119
|
},
|
|
112
120
|
};
|
|
113
121
|
return {
|
|
@@ -129,7 +137,7 @@ export function handleGetDomainCatalog(
|
|
|
129
137
|
content: [
|
|
130
138
|
{
|
|
131
139
|
type: "text" as const,
|
|
132
|
-
text: JSON.stringify(catalog, null, 2),
|
|
140
|
+
text: JSON.stringify({ ...catalog, index_status: indexStatus }, null, 2),
|
|
133
141
|
},
|
|
134
142
|
],
|
|
135
143
|
};
|
|
@@ -139,7 +147,7 @@ function recordCatalogAccess(domain: string, projectRoot: string = process.cwd()
|
|
|
139
147
|
try {
|
|
140
148
|
const septumDir = path.resolve(projectRoot, ".septum");
|
|
141
149
|
if (!fs.existsSync(septumDir)) {
|
|
142
|
-
|
|
150
|
+
return;
|
|
143
151
|
}
|
|
144
152
|
const logPath = path.join(septumDir, "catalog_access.json");
|
|
145
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 {
|
|
@@ -100,18 +100,41 @@ export async function handleLocateSymbol(
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
const exactLineCount = result.exact_symbol
|
|
104
|
+
? result.exact_symbol.line_end - result.exact_symbol.line_start + 1
|
|
105
|
+
: 0;
|
|
106
|
+
const complexitySmell =
|
|
107
|
+
exactLineCount > 300
|
|
108
|
+
? {
|
|
109
|
+
is_high_complexity: true,
|
|
110
|
+
threshold: 300,
|
|
111
|
+
total_lines: exactLineCount,
|
|
112
|
+
warning: `Symbol '${result.exact_symbol?.name}' spans ${exactLineCount} lines (exceeds 300-line threshold). Candidate for decomposition/refactoring.`,
|
|
113
|
+
}
|
|
114
|
+
: undefined;
|
|
115
|
+
|
|
103
116
|
const suggestions = (result.suggestions || []).slice(0, limit);
|
|
104
|
-
|
|
117
|
+
let summary = result.found
|
|
105
118
|
? `[FOUND] ${result.exact_symbol?.name || query} in ${result.file_path} (lines ${result.exact_symbol?.line_start}-${result.exact_symbol?.line_end})`
|
|
106
119
|
: `[NOT FOUND] Symbol '${query}' not found. ${suggestions.length > 0 ? `Top suggestions: ${suggestions.map((s) => s.name).slice(0, 3).join(", ")}` : "No close suggestions."}`;
|
|
107
120
|
|
|
121
|
+
if (complexitySmell) {
|
|
122
|
+
summary += ` | ⚠️ High-Complexity Smell: spans ${exactLineCount} lines (> 300L)`;
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
const payload = {
|
|
109
126
|
status: result.found ? "success" : "not_found",
|
|
110
127
|
found: result.found,
|
|
111
128
|
query,
|
|
112
129
|
file_path: result.file_path || null,
|
|
113
130
|
container_name: result.container_name || null,
|
|
114
|
-
exact_symbol: result.exact_symbol
|
|
131
|
+
exact_symbol: result.exact_symbol
|
|
132
|
+
? {
|
|
133
|
+
...result.exact_symbol,
|
|
134
|
+
total_lines: exactLineCount,
|
|
135
|
+
}
|
|
136
|
+
: null,
|
|
137
|
+
complexity_smell: complexitySmell,
|
|
115
138
|
sibling_methods: result.sibling_methods || [],
|
|
116
139
|
suggestions,
|
|
117
140
|
summary,
|
|
@@ -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,
|