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.
Files changed (72) hide show
  1. package/README.md +38 -37
  2. package/package.json +6 -5
  3. package/src/cli/commands/check.ts +34 -122
  4. package/src/cli/commands/hook.ts +68 -16
  5. package/src/cli/commands/init.ts +440 -121
  6. package/src/cli/commands/sync.ts +27 -17
  7. package/src/cli/commands/topology.ts +89 -47
  8. package/src/cli/index.ts +7 -68
  9. package/src/cli/scaffold/harness-config.ts +697 -0
  10. package/src/cli/templates/hooks.ts +6 -6
  11. package/src/cli/templates/rules.ts +8 -8
  12. package/src/cli/templates/skill.ts +11 -18
  13. package/src/cli/ui/prompts.ts +122 -0
  14. package/src/core/boundary/evaluator.ts +21 -25
  15. package/src/core/boundary/staged-auditor.ts +50 -0
  16. package/src/core/config/loader.ts +21 -24
  17. package/src/core/database/client.ts +8 -0
  18. package/src/core/database/repositories/dependency.repository.ts +80 -16
  19. package/src/core/database/repositories/domain.repository.ts +86 -33
  20. package/src/core/database/repositories/symbol.repository.ts +147 -65
  21. package/src/core/database/repository.ts +17 -5
  22. package/src/core/database/schema.ts +1 -1
  23. package/src/core/discovery/topology-detector.ts +71 -19
  24. package/src/core/ingestion/pipeline.ts +274 -126
  25. package/src/core/parser/extractors/frontend.ts +1 -0
  26. package/src/core/parser/extractors/go.ts +2 -2
  27. package/src/core/parser/extractors/laravel-semantic.ts +9 -8
  28. package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
  29. package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
  30. package/src/core/parser/extractors/php.ts +2 -2
  31. package/src/core/parser/extractors/python.ts +2 -2
  32. package/src/core/parser/extractors/typescript.ts +2 -2
  33. package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
  34. package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
  35. package/src/core/parser/tree-sitter.ts +6 -1
  36. package/src/core/parser/wasm-assets.ts +17 -0
  37. package/src/core/parser/wasm-loader.ts +3 -0
  38. package/src/core/resolver/call-graph-tracer.ts +4 -3
  39. package/src/core/resolver/module-resolver.ts +1 -10
  40. package/src/core/resolver/path-resolver.ts +57 -4
  41. package/src/core/resolver/vertical-slice-tracer.ts +1 -0
  42. package/src/core/resources/ignore.ts +35 -0
  43. package/src/core/resources/limits.ts +14 -0
  44. package/src/core/resources/source-files.ts +62 -0
  45. package/src/core/resources/work-queue.ts +74 -0
  46. package/src/core/session/session-manager.ts +67 -38
  47. package/src/core/telemetry/telemetry.ts +12 -1
  48. package/src/core/topology/environment-detector.ts +21 -11
  49. package/src/mcp/schemas.ts +381 -8
  50. package/src/mcp/server.ts +326 -529
  51. package/src/mcp/tools/boundary.ts +17 -0
  52. package/src/mcp/tools/check-boundary.ts +97 -43
  53. package/src/mcp/tools/domain.ts +40 -0
  54. package/src/mcp/tools/feature.ts +67 -0
  55. package/src/mcp/tools/get-domain-catalog.ts +15 -7
  56. package/src/mcp/tools/get-environment-topology.ts +2 -2
  57. package/src/mcp/tools/get-feature-context.ts +9 -12
  58. package/src/mcp/tools/get-symbol-impact.ts +5 -2
  59. package/src/mcp/tools/get-symbol.ts +2 -1
  60. package/src/mcp/tools/locate-symbol.ts +28 -5
  61. package/src/mcp/tools/register-domain.ts +25 -9
  62. package/src/mcp/tools/register-feature.ts +40 -21
  63. package/src/mcp/tools/slice.ts +15 -0
  64. package/src/mcp/tools/symbol.ts +82 -0
  65. package/src/mcp/workspace-manager.ts +159 -0
  66. package/src/mcp/workspace-watcher.ts +202 -0
  67. package/src/types/index.ts +21 -1
  68. package/src/types/wasm.d.ts +4 -0
  69. package/src/cli/commands/ingest.ts +0 -29
  70. package/src/cli/commands/locate.ts +0 -61
  71. package/src/cli/commands/query.ts +0 -104
  72. 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 { execSync } from "node:child_process";
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 { normalizeBoundaryArgs, type CheckBoundaryArgs } from "../schemas.ts";
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 = (normalizeBoundaryArgs(rawArgs ?? {}) ?? {}) as CheckBoundaryArgs & Record<string, unknown>;
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
- const featureKey = args.feature_key;
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
- let stagedFiles: string[] = [];
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 'septum_register_domain' to initialize bounded contexts.]"
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, featureKey, config);
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 'septum_register_domain' to initialize bounded contexts.]`
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, featureKey, config);
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 'septum_register_domain' to initialize bounded contexts."
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 'septum_register_domain' to initialize bounded contexts.]"
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: "septum_register_domain",
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 '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.",
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 'septum_get_domain_catalog' with domain='<name>' for granular file/symbol catalogs, or 'septum_trace_vertical_slice' with query='<route|command|intent>' for end-to-end execution flow.",
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: "septum_register_domain",
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
- fs.mkdirSync(septumDir, { recursive: true });
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 += `\`septum_get_environment_topology(resolve: { layer: "<layer_name>", platform: "<platform_name>" })\`\n`;
125
+ output += `\`septum_domain(action: "topology", resolve: { layer: "<layer_name>", platform: "<platform_name>" })\`\n`;
126
126
  output += `*atau untuk entitas terdistribusi:*\n`;
127
- output += `\`septum_get_environment_topology(resolveNode: { id: "<node_id>", layer: "<layer>", name: "<name>", platform: "<platform>", domain_or_ip: "<domain_or_ip>", role: "<role>" })\`\n`;
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(workspaceRoot: string = process.cwd()): boolean {
12
- return SessionManager.clearActiveSession(workspaceRoot);
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 'septum_get_feature_context({ feature_key: \"<name>\" })' or register a new feature with 'septum_register_feature'.",
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
- `septum_register_feature({ feature_key: "${featureKey}", domain: "<domain_name>", allowed_touchpoints: ["path/to/file"] })`,
131
- `septum_trace_vertical_slice({ entry_file: "routes/web.php" })`,
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
- // Record Active Feature Session Lease (.septum/session.json)
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
- _workspaceRoot?: string
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
- _workspaceRoot?: string
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: "septum_register_domain",
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 'septum_register_domain(name: \"core\", root: \"src\", ingest_now: true)' untuk mendaftarkan dan mengindeks codebase ini, atau jalankan 'septum init' di terminal workspace.",
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
- const summary = result.found
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 || null,
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[domainName] = domainConfig;
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 '${domainName}' successfully registered in SQLite SSOT.${
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: domainName,
49
- name: domainName,
50
- config: domainConfig,
64
+ domain: canonicalName,
65
+ name: canonicalName,
66
+ config: finalConfig,
51
67
  ...(ingestionMetrics ? { ingestion_metrics: ingestionMetrics } : {}),
52
68
  },
53
69
  null,