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.
Files changed (59) hide show
  1. package/README.md +36 -55
  2. package/package.json +3 -2
  3. package/src/cli/commands/check.ts +2 -0
  4. package/src/cli/commands/hook.ts +59 -12
  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 +5 -67
  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/staged-auditor.ts +5 -2
  15. package/src/core/database/client.ts +8 -0
  16. package/src/core/database/repositories/dependency.repository.ts +77 -13
  17. package/src/core/database/repositories/domain.repository.ts +86 -33
  18. package/src/core/database/repositories/symbol.repository.ts +128 -117
  19. package/src/core/database/repository.ts +17 -5
  20. package/src/core/database/schema.ts +1 -1
  21. package/src/core/discovery/topology-detector.ts +71 -19
  22. package/src/core/ingestion/pipeline.ts +228 -94
  23. package/src/core/parser/extractors/laravel-semantic.ts +9 -8
  24. package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
  25. package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
  26. package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
  27. package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
  28. package/src/core/resolver/call-graph-tracer.ts +4 -3
  29. package/src/core/resolver/path-resolver.ts +57 -4
  30. package/src/core/resources/ignore.ts +35 -0
  31. package/src/core/resources/limits.ts +14 -0
  32. package/src/core/resources/source-files.ts +62 -0
  33. package/src/core/resources/work-queue.ts +74 -0
  34. package/src/core/session/session-manager.ts +19 -6
  35. package/src/core/telemetry/telemetry.ts +12 -1
  36. package/src/core/topology/environment-detector.ts +21 -11
  37. package/src/mcp/schemas.ts +381 -8
  38. package/src/mcp/server.ts +321 -535
  39. package/src/mcp/tools/boundary.ts +17 -0
  40. package/src/mcp/tools/check-boundary.ts +83 -11
  41. package/src/mcp/tools/domain.ts +40 -0
  42. package/src/mcp/tools/feature.ts +67 -0
  43. package/src/mcp/tools/get-domain-catalog.ts +9 -6
  44. package/src/mcp/tools/get-environment-topology.ts +2 -2
  45. package/src/mcp/tools/get-feature-context.ts +9 -12
  46. package/src/mcp/tools/get-symbol-impact.ts +5 -2
  47. package/src/mcp/tools/get-symbol.ts +2 -1
  48. package/src/mcp/tools/locate-symbol.ts +3 -3
  49. package/src/mcp/tools/register-domain.ts +25 -9
  50. package/src/mcp/tools/register-feature.ts +34 -17
  51. package/src/mcp/tools/slice.ts +15 -0
  52. package/src/mcp/tools/symbol.ts +82 -0
  53. package/src/mcp/workspace-manager.ts +159 -0
  54. package/src/mcp/workspace-watcher.ts +202 -0
  55. package/src/types/index.ts +1 -0
  56. package/src/cli/commands/ingest.ts +0 -29
  57. package/src/cli/commands/locate.ts +0 -61
  58. package/src/cli/commands/query.ts +0 -104
  59. 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 { normalizeBoundaryArgs, type CheckBoundaryArgs } from "../schemas.ts";
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 = (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
+ }
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, featureKey, repo);
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 '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.]"
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, featureKey, config);
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 '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.]`
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, featureKey, config);
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 '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."
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 '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.]"
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: "septum_register_domain",
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 '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.",
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 '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.",
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: "septum_register_domain",
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
- fs.mkdirSync(septumDir, { recursive: true });
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 += `\`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 {
@@ -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,
@@ -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
+ }