septum 0.1.6 → 0.1.7

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 CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  > **Deterministic Bounded-Context & Anti-Poisoning Architecture Guard for AI Coding Agents.**
4
4
 
5
- [![Version: 0.1.5](https://img.shields.io/badge/Version-0.1.5-blue)](package.json)
5
+ [![Version: 0.1.7](https://img.shields.io/badge/Version-0.1.7-blue)](package.json)
6
6
  [![Runtime: Bun](https://img.shields.io/badge/Runtime-Bun-black?logo=bun)](https://bun.sh)
7
7
  [![Protocol: Model Context Protocol](https://img.shields.io/badge/Protocol-MCP-green)](https://modelcontextprotocol.io/)
8
8
  [![Database: SQLite WAL](https://img.shields.io/badge/Database-SQLite%20(bun:sqlite)-003B57?logo=sqlite)](https://sqlite.org/)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue)](LICENSE)
10
10
 
11
- Septum stops **context poisoning** in AI coding agents. It indexes your codebase into an embedded SQLite SSOT (`.septum/septum.db`), enforces strict bounded-context domains, and intercepts unauthorized cross-domain edits before they hit disk.
11
+ Septum helps AI coding agents navigate code and respect declared architectural boundaries. It indexes your codebase into an embedded SQLite SSOT (`.septum/septum.db`), enforces strict bounded-context domains, and intercepts unauthorized cross-domain edits before they hit disk.
12
12
 
13
13
  ---
14
14
 
@@ -59,7 +59,7 @@ Add Septum to your MCP client (`mcp.json` or `claude_desktop_config.json`):
59
59
  |---|---|
60
60
  | `septum init` | Detect topology, scaffold boundary guards, and build initial catalog |
61
61
  | `septum ingest` | Full AST re-indexing into `.septum/septum.db` |
62
- | `septum sync` | Fast incremental delta sync (< 15ms) for modified files |
62
+ | `septum sync` | Incremental synchronization of modified files for modified files |
63
63
  | `septum check --strict` | CI/CD gate: verify boundary integrity & fail on violations |
64
64
  | `septum query [domain]` | Inspect domain catalog, archetypes, and public signatures |
65
65
  | `septum locate <query>` | Locate symbol, class, interface, method, or error source |
@@ -103,3 +103,23 @@ bun run build
103
103
  ## License
104
104
 
105
105
  [MIT](LICENSE) © shironim
106
+
107
+ ## Reliability and Scope
108
+
109
+ `septum check --strict` enables strict dependency rules before evaluating the codebase. A full check synchronizes the index first. CLI and MCP staged checks read Git index blobs, so unstaged edits cannot hide a violation in the proposed commit. Staged checks also evaluate dependency cycles using indexed dependencies with staged imports overlaid.
110
+
111
+ Domain ownership uses the most specific normalized root in both full and single-file ingestion. Feature touchpoints are normalized against the workspace; `/*` permits immediate children and `/**` permits descendants.
112
+
113
+ Each active MCP workspace has a watcher. Changes to multiple files are batched without dropping earlier paths; deletion and directory moves reconcile the catalog. If a native watcher is unavailable, Septum falls back to polling and synchronizes before answering queries. Catalog responses expose `index_status.watcher_mode` and `index_status.unsupported_files`. Single-file updates invalidate cached execution slices; the next trace rebuilds them.
114
+
115
+ Tree-sitter grammars are runtime dependencies and are embedded in compiled executables. The active extractors cover TypeScript/JavaScript, PHP/Blade, Python, Go, and script extraction from Vue/Svelte/Astro. Rust, Java, C#, and Ruby do not yet have active extractors. Unsupported files are reported explicitly; an incomplete audit does not receive a clean approval. Unindexed symbols return `risk_level: "unknown"`.
116
+
117
+ Feature sessions have a single owner per workspace. The default owner is the MCP process; set `SEPTUM_SESSION_ID` when several commands intentionally share the same owner. Another owner cannot replace or release the active session. Use `septum feature clear --force` for explicit recovery after a process exits or for a legacy session without ownership metadata.
118
+
119
+ Run the real-server integration checks with Python 3:
120
+
121
+ ```bash
122
+ bun run test:integration
123
+ ```
124
+
125
+ These checks exercise live rule changes, Git staged blobs, multiple workspaces, rapid edits, directory moves, and unsupported-file diagnostics. The normal `bun test` suite includes a standalone-binary regression for forbidden multiline imports.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "septum",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Deterministic Bounded-Context, Structural File Catalog & Anti-Poisoning Partition for AI Agents",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -18,13 +18,15 @@
18
18
  "check": "bun run bin/septum.ts check",
19
19
  "test": "bun test",
20
20
  "build": "bun build ./bin/septum.ts --compile --outfile dist/septum",
21
- "typecheck": "tsc --noEmit"
21
+ "typecheck": "tsc --noEmit",
22
+ "test:integration": "python3 tests/integration/mcp-reliability.py"
22
23
  },
23
24
  "dependencies": {
24
25
  "@modelcontextprotocol/sdk": "^1.5.0",
25
26
  "web-tree-sitter": "^0.24.0",
26
27
  "yaml": "^2.7.0",
27
- "zod": "^3.24.2"
28
+ "zod": "^3.24.2",
29
+ "tree-sitter-wasms": "^0.1.13"
28
30
  },
29
31
  "license": "MIT",
30
32
  "repository": {
@@ -56,8 +58,6 @@
56
58
  "devDependencies": {
57
59
  "@types/bun": "latest",
58
60
  "@types/node": "^22.13.4",
59
- "tree-sitter-wasms": "^0.1.13",
60
61
  "typescript": "^5.7.3"
61
62
  }
62
63
  }
63
-
@@ -1,13 +1,10 @@
1
- import { execSync } from "node:child_process";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
1
  import { BoundaryEvaluator } from "../../core/boundary/evaluator.ts";
5
- import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
6
2
  import { ConfigLoader } from "../../core/config/loader.ts";
7
3
  import { SeptumDatabase } from "../../core/database/client.ts";
8
4
  import { SeptumRepository } from "../../core/database/repository.ts";
9
- import { ASTParserEngine } from "../../core/parser/tree-sitter.ts";
10
5
  import type { BoundaryViolation } from "../../types/index.ts";
6
+ import { IngestionPipeline } from "../../core/ingestion/pipeline.ts";
7
+ import { auditStagedFiles } from "../../core/boundary/staged-auditor.ts";
11
8
 
12
9
  export interface CheckCommandOptions {
13
10
  strict?: boolean;
@@ -28,134 +25,47 @@ export async function handleCheckCommand(
28
25
  );
29
26
 
30
27
  const config = ConfigLoader.load();
28
+ if (options.strict) config.settings.enforcement = "strict";
31
29
  const db = new SeptumDatabase(config.settings.db_path);
32
30
  const repo = new SeptumRepository(db.raw);
33
31
  const evaluator = new BoundaryEvaluator(repo);
34
32
 
35
- let violations: BoundaryViolation[] = [];
36
-
37
- if (options.staged) {
38
- violations = await auditStagedFiles(evaluator, config, options.feature);
39
- } else {
40
- violations = evaluator.auditCodebase(config);
41
- }
42
-
43
- if (violations.length === 0) {
44
- console.log("✓ All domain boundaries are clean. Zero cross-domain leaks detected.");
45
- db.close();
46
- return;
47
- }
48
-
49
- console.error(`\n[Septum Boundary Violation] Found ${violations.length} boundary violation(s):\n`);
50
-
51
- for (const v of violations) {
52
- console.error(` ✕ [${v.rule.toUpperCase()}] at ${v.file}:${v.line}`);
53
- console.error(` Source Domain: ${v.source_domain}`);
54
- console.error(` Target Domain: ${v.target_domain}`);
55
- console.error(` Import Target: ${v.imported_target}`);
56
- console.error(` Message: ${v.message}\n`);
57
- }
58
-
59
- db.close();
60
-
61
- const isStrict = options.strict || config.settings.enforcement === "strict";
62
- if (isStrict) {
63
- console.error("[Septum Failure] Process terminated with exit code 1 due to boundary violations.");
64
- process.exit(1);
65
- }
66
- }
67
-
68
- async function auditStagedFiles(
69
- evaluator: BoundaryEvaluator,
70
- config: ValidatedSeptumConfig,
71
- featureKey?: string
72
- ): Promise<BoundaryViolation[]> {
73
- const violations: BoundaryViolation[] = [];
74
- let stagedFiles: string[] = [];
75
-
76
33
  try {
77
- const gitOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
78
- encoding: "utf-8",
79
- timeout: 5000,
80
- maxBuffer: 10 * 1024 * 1024,
81
- });
82
- stagedFiles = gitOutput
83
- .split("\n")
84
- .map((f) => f.trim())
85
- .filter(Boolean);
86
- } catch (err) {
87
- const msg = err instanceof Error ? err.message : String(err);
88
- console.warn(`[Septum Warning] Could not retrieve git staged files: ${msg}`);
89
- return violations;
90
- }
91
-
92
- if (stagedFiles.length === 0) {
93
- console.log(" Notice: No staged files found in git repository.");
94
- return violations;
95
- }
96
-
97
- const astEngine = new ASTParserEngine();
98
-
99
- for (const relPath of stagedFiles) {
100
- const fullPath = path.resolve(process.cwd(), relPath);
101
- if (!fs.existsSync(fullPath)) continue;
102
-
103
- // 1. Feature touchpoint check if featureKey provided
104
- if (featureKey) {
105
- const tpViolations = evaluator.checkFeatureTouchpoints(featureKey, relPath, config);
106
- violations.push(...tpViolations);
34
+ let violations: BoundaryViolation[] = [];
35
+
36
+ if (options.staged) {
37
+ violations = (await auditStagedFiles(evaluator, config, process.cwd(), options.feature, repo)).violations;
38
+ } else {
39
+ await new IngestionPipeline(repo).run(config, { verifyContent: true });
40
+ const unsupported = JSON.parse(repo.getMeta("unsupported_files") || "[]") as string[];
41
+ if (unsupported.length > 0) {
42
+ throw new Error(`Cannot complete boundary audit: unsupported source files: ${unsupported.join(", ")}`);
43
+ }
44
+ violations = evaluator.auditCodebase(config);
107
45
  }
108
46
 
109
- const sourceDomain = evaluator.resolveSourceDomain(relPath, config);
110
- if (!sourceDomain) continue;
111
-
112
- const sourceConfig = config.domains[sourceDomain];
113
- if (!sourceConfig) continue;
114
-
115
- const forbidden = sourceConfig.forbidden_dependencies ?? [];
116
- const allowed = sourceConfig.allowed_dependencies ?? [];
117
-
118
- try {
119
- const content = fs.readFileSync(fullPath, "utf-8");
120
- const parsed = await astEngine.parseFile(fullPath, content);
47
+ if (violations.length === 0) {
48
+ console.log("✓ All domain boundaries are clean. Zero cross-domain leaks detected.");
49
+ return;
50
+ }
121
51
 
122
- for (const dep of parsed.dependencies) {
123
- if (dep.is_external) continue;
52
+ console.error(`\n[Septum Boundary Violation] Found ${violations.length} boundary violation(s):\n`);
124
53
 
125
- const targetDomain = evaluator.resolveTargetDomain(
126
- dep.target,
127
- config,
128
- relPath
129
- );
54
+ for (const v of violations) {
55
+ console.error(` ✕ [${v.rule.toUpperCase()}] at ${v.file}:${v.line}`);
56
+ console.error(` Source Domain: ${v.source_domain}`);
57
+ console.error(` Target Domain: ${v.target_domain}`);
58
+ console.error(` Import Target: ${v.imported_target}`);
59
+ console.error(` Message: ${v.message}\n`);
60
+ }
130
61
 
131
- if (!targetDomain || targetDomain === sourceDomain) continue;
132
62
 
133
- if (forbidden.includes(targetDomain)) {
134
- violations.push({
135
- file: relPath,
136
- line: dep.line_number,
137
- source_domain: sourceDomain,
138
- target_domain: targetDomain,
139
- imported_target: dep.target,
140
- rule: "forbidden_dependency",
141
- message: `Illegal import: Domain '${sourceDomain}' is strictly prohibited from importing domain '${targetDomain}'.`,
142
- });
143
- } else if (config.settings.enforcement === "strict" && !allowed.includes(targetDomain)) {
144
- violations.push({
145
- file: relPath,
146
- line: dep.line_number,
147
- source_domain: sourceDomain,
148
- target_domain: targetDomain,
149
- imported_target: dep.target,
150
- rule: "disallowed_dependency",
151
- message: `Disallowed dependency: '${targetDomain}' is not declared in allowed_dependencies for domain '${sourceDomain}'.`,
152
- });
153
- }
154
- }
155
- } catch {
156
- // Ignore unparseable files
63
+ const isStrict = options.strict || config.settings.enforcement === "strict";
64
+ if (isStrict) {
65
+ console.error("[Septum Failure] Process terminated with exit code 1 due to boundary violations.");
66
+ process.exitCode = 1;
157
67
  }
68
+ } finally {
69
+ db.close();
158
70
  }
159
-
160
- return violations;
161
71
  }
@@ -130,12 +130,14 @@ async function runPreWriteHook(): Promise<void> {
130
130
  // -------------------------------------------------------------
131
131
  // GATE 1: Hard Touchpoint Lock (if features are defined)
132
132
  // -------------------------------------------------------------
133
- const features = config.features ?? {};
134
133
  const session = SessionManager.getActiveSession(process.cwd());
135
- const activeFeatureKey =
136
- toolInput.feature_key || process.env.SEPTUM_ACTIVE_FEATURE || session?.feature_key;
134
+ const requestedFeatureKey = toolInput.feature_key || process.env.SEPTUM_ACTIVE_FEATURE;
135
+ if (session && requestedFeatureKey && requestedFeatureKey !== session.feature_key) {
136
+ throw new Error(`Requested feature '${requestedFeatureKey}' does not own the active scope '${session.feature_key}'.`);
137
+ }
138
+ const activeFeatureKey = session?.feature_key || requestedFeatureKey;
137
139
 
138
- if (activeFeatureKey && features[activeFeatureKey]) {
140
+ if (activeFeatureKey) {
139
141
  const touchpointViolations = evaluator.checkFeatureTouchpoints(
140
142
  activeFeatureKey,
141
143
  targetFile,
@@ -161,6 +163,9 @@ async function runPreWriteHook(): Promise<void> {
161
163
  if (content && sourceDomain) {
162
164
  const astEngine = new ASTParserEngine();
163
165
  const parsed = await astEngine.parseFile(targetFile, content);
166
+ if (ASTParserEngine.isSourceFile(targetFile) && (parsed.status === "unsupported" || parsed.status === "degraded")) {
167
+ throw new Error(`Cannot reliably check imports in '${targetFile}': parser status is ${parsed.status}.`);
168
+ }
164
169
 
165
170
  const proposedImports = parsed.dependencies.map((d) => d.target);
166
171
  const boundaryViolations = evaluator.checkProposedChanges(
package/src/cli/index.ts CHANGED
@@ -132,7 +132,7 @@ export async function runCLI(argv: string[]): Promise<void> {
132
132
  case "feature": {
133
133
  const sub = positionals[1];
134
134
  if (sub === "clear") {
135
- const cleared = SessionManager.clearActiveSession(process.cwd());
135
+ const cleared = SessionManager.clearActiveSession(process.cwd(), undefined, Boolean(flags.force));
136
136
  if (cleared) {
137
137
  console.log("✓ Active feature session successfully cleared.");
138
138
  } else {
@@ -203,6 +203,7 @@ COMMANDS:
203
203
  check Audit codebase for cross-domain boundary violations
204
204
  Flags: --strict, --staged, --feature <name>
205
205
  feature [status|clear] View or release active feature context session lock
206
+ Flags: --force (explicit recovery of another owner's session)
206
207
  hook [subhook] Internal pre-tool guardrail hook (e.g. pre-write)
207
208
 
208
209
  Environment & Topology:
@@ -1,4 +1,4 @@
1
- import { posix } from "node:path";
1
+ import { posix, relative, resolve, isAbsolute } from "node:path";
2
2
  import type { BoundaryViolation } from "../../types/index.ts";
3
3
  import type { ValidatedSeptumConfig } from "../config/schema.ts";
4
4
  import { ConfigLoader } from "../config/loader.ts";
@@ -105,7 +105,8 @@ export class BoundaryEvaluator {
105
105
  target: string;
106
106
  statement: string;
107
107
  is_external: boolean;
108
- }>
108
+ }>,
109
+ relevantFiles?: ReadonlySet<string>
109
110
  ): BoundaryViolation[] {
110
111
  const violations: BoundaryViolation[] = [];
111
112
  const allDependencies = preloadedDeps ?? this.repo.getAllDependenciesWithDomains();
@@ -158,6 +159,7 @@ export class BoundaryEvaluator {
158
159
  };
159
160
 
160
161
  type EdgeMeta = {
162
+ files: Set<string>;
161
163
  file: string;
162
164
  line: number;
163
165
  source_domain: string;
@@ -182,6 +184,7 @@ export class BoundaryEvaluator {
182
184
  }
183
185
  if (!outMap.has(targetDomain)) {
184
186
  outMap.set(targetDomain, {
187
+ files: new Set([dep.source_file]),
185
188
  file: dep.source_file,
186
189
  line: dep.line_number,
187
190
  source_domain: dep.source_domain,
@@ -189,6 +192,7 @@ export class BoundaryEvaluator {
189
192
  target: dep.target,
190
193
  });
191
194
  }
195
+ outMap.get(targetDomain)!.files.add(dep.source_file);
192
196
  }
193
197
 
194
198
  // 2. File-to-file edge
@@ -201,6 +205,7 @@ export class BoundaryEvaluator {
201
205
  }
202
206
  if (!outMap.has(targetFile)) {
203
207
  outMap.set(targetFile, {
208
+ files: new Set([dep.source_file]),
204
209
  file: dep.source_file,
205
210
  line: dep.line_number,
206
211
  source_domain: dep.source_domain,
@@ -245,6 +250,8 @@ export class BoundaryEvaluator {
245
250
  if (cycleStartIndex !== -1) {
246
251
  const cycleNodes = stack.slice(cycleStartIndex);
247
252
  const sig = canonicalizeCycle(cycleNodes);
253
+ const cycleEdges = cycleNodes.map((source, index) => graph.get(source)?.get(cycleNodes[(index + 1) % cycleNodes.length]));
254
+ if (relevantFiles && !cycleEdges.some(edge => edge && [...edge.files].some(file => relevantFiles.has(file)))) continue;
248
255
  if (!reportedCycles.has(sig)) {
249
256
  reportedCycles.add(sig);
250
257
  const firstNode = cycleNodes[0];
@@ -378,39 +385,28 @@ export class BoundaryEvaluator {
378
385
  return violations;
379
386
  }
380
387
 
381
- const normalizedTarget = targetFilePath.replace(/\\/g, "/").replace(/^\.\//, "");
388
+ const root = this.resolver.getProjectRoot();
389
+ const normalizedTarget = relative(root, resolve(root, targetFilePath.replace(/\\/g, "/"))).replace(/\\/g, "/");
382
390
  const allowedTouchpoints = featureConfig.allowed_touchpoints.map((tp) =>
383
- tp.replace(/\\/g, "/").replace(/^\.\//, "")
391
+ isAbsolute(tp) ? relative(root, tp).replace(/\\/g, "/") : posix.normalize(tp.replace(/\\/g, "/"))
384
392
  );
385
393
 
386
394
  const domainCfg = config.domains[featureConfig.domain];
387
395
  const domainRoot = domainCfg?.root ? domainCfg.root.replace(/\\/g, "/").replace(/^\.\//, "") : "";
388
396
 
389
- const isAllowed = allowedTouchpoints.some((pattern) => {
397
+ const matches = (pattern: string): boolean => {
390
398
  if (pattern.endsWith("/**")) {
391
- const prefix = pattern.slice(0, -3);
392
- return normalizedTarget.startsWith(prefix);
399
+ return normalizedTarget.startsWith(pattern.slice(0, -3) + "/");
393
400
  }
394
401
  if (pattern.endsWith("/*")) {
395
- const prefix = pattern.slice(0, -2);
396
- return normalizedTarget.startsWith(prefix);
402
+ const prefix = pattern.slice(0, -2) + "/";
403
+ return normalizedTarget.startsWith(prefix) && !normalizedTarget.slice(prefix.length).includes("/");
397
404
  }
398
- if (normalizedTarget === pattern) return true;
399
-
400
- if (
401
- domainRoot &&
402
- (normalizedTarget === `${domainRoot}/${pattern}` ||
403
- normalizedTarget.startsWith(`${domainRoot}/${pattern}`))
404
- ) {
405
- return true;
406
- }
407
-
408
- if (pattern.includes("/") && normalizedTarget.endsWith("/" + pattern)) {
409
- return true;
410
- }
411
-
412
- return false;
413
- });
405
+ return normalizedTarget === pattern;
406
+ };
407
+ const isAllowed = allowedTouchpoints.some(pattern =>
408
+ matches(pattern) || Boolean(domainRoot && matches(posix.normalize(`${domainRoot}/${pattern}`)))
409
+ );
414
410
 
415
411
  if (!isAllowed) {
416
412
  violations.push({
@@ -0,0 +1,47 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import type { BoundaryViolation } from "../../types/index.ts";
4
+ import type { ValidatedSeptumConfig } from "../config/schema.ts";
5
+ import { ASTParserEngine } from "../parser/tree-sitter.ts";
6
+ import type { SeptumRepository } from "../database/repository.ts";
7
+ import type { BoundaryEvaluator } from "./evaluator.ts";
8
+
9
+ export async function auditStagedFiles(
10
+ evaluator: BoundaryEvaluator,
11
+ config: ValidatedSeptumConfig,
12
+ workspaceRoot: string,
13
+ featureKey?: string,
14
+ repo?: SeptumRepository
15
+ ): Promise<{ files: string[]; violations: BoundaryViolation[] }> {
16
+ const output = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"], {
17
+ cwd: workspaceRoot, encoding: "utf-8", timeout: 5000, maxBuffer: 10 * 1024 * 1024,
18
+ });
19
+ const files = output.split("\0").filter(Boolean);
20
+ const deletedOutput = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], {
21
+ cwd: workspaceRoot, encoding: "utf-8", timeout: 5000, maxBuffer: 10 * 1024 * 1024,
22
+ });
23
+ const deleted = deletedOutput.split("\0").filter(Boolean);
24
+ const violations: BoundaryViolation[] = [];
25
+ if (featureKey) for (const file of deleted) violations.push(...evaluator.checkFeatureTouchpoints(featureKey, file, config));
26
+ const stagedDependencies: ReturnType<SeptumRepository["getAllDependenciesWithDomains"]> = [];
27
+ const parser = new ASTParserEngine();
28
+ for (const file of files) {
29
+ if (featureKey) violations.push(...evaluator.checkFeatureTouchpoints(featureKey, file, config));
30
+ if (!ASTParserEngine.isSourceFile(file) || !evaluator.resolveSourceDomain(file, config)) continue;
31
+ const content = execFileSync("git", ["show", `:${file}`], {
32
+ cwd: workspaceRoot, encoding: "utf-8", timeout: 5000, maxBuffer: 10 * 1024 * 1024,
33
+ });
34
+ const parsed = await parser.parseFile(resolve(workspaceRoot, file), content);
35
+ if (parsed.status === "unsupported" || parsed.status === "degraded") {
36
+ throw new Error(`Cannot reliably parse staged file '${file}': ${parsed.status}.`);
37
+ }
38
+ for (const dependency of parsed.dependencies) {
39
+ stagedDependencies.push({ source_file: file, source_domain: evaluator.resolveSourceDomain(file, config)!, target: dependency.target, statement: dependency.statement, line_number: dependency.line_number, is_external: dependency.is_external });
40
+ violations.push(...evaluator.checkProposedChanges(file, [dependency.target], config).map(violation => ({ ...violation, line: dependency.line_number })));
41
+ }
42
+ }
43
+ const staged = new Set([...files, ...deleted]);
44
+ const baseline = (repo?.getAllDependenciesWithDomains() || []).filter(dependency => !staged.has(dependency.source_file));
45
+ violations.push(...evaluator.detectCircularDependencies(config, [...baseline, ...stagedDependencies], staged));
46
+ return { files, violations };
47
+ }
@@ -65,20 +65,23 @@ export class ConfigLoader {
65
65
  public static loadFromRepository(repo: SeptumRepository): ValidatedSeptumConfig {
66
66
  const config = ConfigLoader.getDefaultConfig();
67
67
  if (repo && typeof repo.getAllDomains === "function") {
68
- try {
69
- const rows = repo.getAllDomains();
70
- for (const r of rows) {
71
- config.domains[r.name] = {
72
- root: r.root_path,
73
- allowed_dependencies: JSON.parse(r.allowed_deps_json || "[]"),
74
- forbidden_dependencies: JSON.parse(r.forbidden_deps_json || "[]"),
75
- archetypes: JSON.parse(r.archetypes_json || "{}"),
76
- };
77
- }
78
- } catch {
79
- // Fallback gracefully to default config
68
+ const rows = repo.getAllDomains();
69
+ for (const row of rows) {
70
+ config.domains[row.name] = {
71
+ root: row.root_path,
72
+ allowed_dependencies: JSON.parse(row.allowed_deps_json || "[]"),
73
+ forbidden_dependencies: JSON.parse(row.forbidden_deps_json || "[]"),
74
+ archetypes: JSON.parse(row.archetypes_json || "{}"),
75
+ };
80
76
  }
81
77
  }
78
+
79
+ if (typeof repo?.getMeta === "function") {
80
+ const features = repo.getMeta("features_json");
81
+ if (features) config.features = JSON.parse(features);
82
+ const settings = repo.getMeta("settings_json");
83
+ if (settings) Object.assign(config.settings, JSON.parse(settings));
84
+ }
82
85
  return config;
83
86
  }
84
87
 
@@ -95,20 +98,14 @@ export class ConfigLoader {
95
98
  )
96
99
  .get("domains");
97
100
 
98
- if (tables) {
99
- const rows = db.query<any, []>("SELECT * FROM domains").all();
100
- for (const r of rows) {
101
- config.domains[r.name] = {
102
- root: r.root_path,
103
- allowed_dependencies: JSON.parse(r.allowed_deps_json || "[]"),
104
- forbidden_dependencies: JSON.parse(r.forbidden_deps_json || "[]"),
105
- archetypes: JSON.parse(r.archetypes_json || "{}"),
106
- };
107
- }
101
+ try {
102
+ if (tables) Object.assign(config, ConfigLoader.loadFromRepository(new SeptumRepository(db)));
103
+ config.settings.db_path = dbPath;
104
+ } finally {
105
+ db.close();
108
106
  }
109
- db.close();
110
107
  } catch {
111
- // Fallback gracefully to default config
108
+ throw new Error(`Cannot load Septum configuration from '${dbPath}'.`);
112
109
  }
113
110
  }
114
111
 
@@ -258,7 +258,7 @@ export class DependencyRepository {
258
258
  warning: "Empty symbol query provided.",
259
259
  impact_summary: {
260
260
  total_dependents: 0,
261
- risk_level: "low",
261
+ risk_level: "unknown",
262
262
  affected_files_count: 0,
263
263
  },
264
264
  inbound_callers: [],
@@ -463,7 +463,7 @@ export class DependencyRepository {
463
463
  const foundInIndex = targetSymRows.length > 0 || depRows.length > 0;
464
464
  const warning = foundInIndex
465
465
  ? undefined
466
- : `Symbol '${trimmed}' was not found in Septum index (neither in declared symbols nor tracked dependencies). Risk assessment defaults to low (0 callers) due to unindexed status.`;
466
+ : `Symbol '${trimmed}' was not found in Septum index (neither in declared symbols nor tracked dependencies). Risk cannot be assessed without index evidence.`;
467
467
 
468
468
  return {
469
469
  target_symbol: trimmed,
@@ -471,7 +471,7 @@ export class DependencyRepository {
471
471
  warning,
472
472
  impact_summary: {
473
473
  total_dependents: inboundCallers.length,
474
- risk_level: riskLevel,
474
+ risk_level: foundInIndex ? riskLevel : "unknown",
475
475
  affected_files_count: affectedFiles.size,
476
476
  },
477
477
  inbound_callers: inboundCallers,
@@ -5,6 +5,8 @@ import type {
5
5
  ExtractedSymbol,
6
6
  FileRecord,
7
7
  GetSymbolDetailsResponse,
8
+ ResolvedOutboundCall,
9
+ SymbolComplexitySmell,
8
10
  SymbolKind,
9
11
  SymbolRecord,
10
12
  } from "../../../types/index.ts";
@@ -460,6 +462,73 @@ export class SymbolRepository {
460
462
  const depRepo = new DependencyRepository(this.db);
461
463
  const connectedFiles = depRepo.getFileConnections(row.file_path);
462
464
 
465
+ // 4. Resolve Outbound Calls to target symbol line ranges
466
+ const resolvedOutboundCalls: ResolvedOutboundCall[] = [];
467
+ const lookupStmt = this.db.query<
468
+ {
469
+ name: string;
470
+ line_start: number;
471
+ line_end: number;
472
+ line_count: number;
473
+ file_path: string;
474
+ domain_name: string;
475
+ },
476
+ [string, string, string, string]
477
+ >(`
478
+ SELECT
479
+ s.name,
480
+ s.line_start,
481
+ s.line_end,
482
+ s.line_count,
483
+ f.path AS file_path,
484
+ d.name AS domain_name
485
+ FROM symbols s
486
+ JOIN files f ON s.file_id = f.id
487
+ JOIN domains d ON f.domain_id = d.id
488
+ WHERE (s.name = ? OR s.name LIKE ? OR s.name LIKE ?)
489
+ ORDER BY CASE WHEN s.name = ? THEN 1 ELSE 2 END, s.line_start ASC
490
+ LIMIT 1
491
+ `);
492
+
493
+ for (const call of outboundCalls) {
494
+ try {
495
+ const target = lookupStmt.get(call, `%::${call}`, `${call}::%`, call);
496
+ if (target) {
497
+ resolvedOutboundCalls.push({
498
+ symbol: call,
499
+ target_file: target.file_path,
500
+ target_range: {
501
+ start_line: target.line_start,
502
+ end_line: target.line_end,
503
+ total_lines: target.line_count,
504
+ },
505
+ domain: target.domain_name,
506
+ is_resolved: true,
507
+ });
508
+ } else {
509
+ resolvedOutboundCalls.push({
510
+ symbol: call,
511
+ is_resolved: false,
512
+ });
513
+ }
514
+ } catch {
515
+ resolvedOutboundCalls.push({
516
+ symbol: call,
517
+ is_resolved: false,
518
+ });
519
+ }
520
+ }
521
+
522
+ const complexitySmell: SymbolComplexitySmell | undefined =
523
+ row.line_count > 300
524
+ ? {
525
+ is_high_complexity: true,
526
+ threshold: 300,
527
+ total_lines: row.line_count,
528
+ warning: `Symbol '${row.name}' spans ${row.line_count} lines (exceeds 300-line threshold). Candidate for decomposition/refactoring.`,
529
+ }
530
+ : undefined;
531
+
463
532
  return {
464
533
  symbol: row.name,
465
534
  kind: row.kind,
@@ -474,6 +543,8 @@ export class SymbolRepository {
474
543
  visibility: row.visibility,
475
544
  dependencies_injected: Array.from(dependenciesInjected),
476
545
  outbound_calls: Array.from(outboundCalls),
546
+ resolved_outbound_calls: resolvedOutboundCalls.length > 0 ? resolvedOutboundCalls : undefined,
547
+ complexity_smell: complexitySmell,
477
548
  sibling_methods: siblingMethods.length > 0 ? siblingMethods : undefined,
478
549
  connected_files: connectedFiles,
479
550
  };