septum 0.1.4 → 0.1.6

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 (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -105
  3. package/bin/septum.ts +4 -4
  4. package/package.json +63 -63
  5. package/src/cli/commands/check.ts +161 -161
  6. package/src/cli/commands/hook.ts +216 -188
  7. package/src/cli/commands/ingest.ts +29 -29
  8. package/src/cli/commands/init.ts +213 -211
  9. package/src/cli/commands/locate.ts +61 -61
  10. package/src/cli/commands/query.ts +104 -64
  11. package/src/cli/commands/serve.ts +5 -5
  12. package/src/cli/commands/slice.ts +50 -48
  13. package/src/cli/commands/sync.ts +31 -31
  14. package/src/cli/index.ts +220 -210
  15. package/src/cli/templates/hooks.ts +75 -75
  16. package/src/cli/templates/rules.ts +50 -38
  17. package/src/cli/templates/skill.ts +65 -69
  18. package/src/core/boundary/evaluator.ts +429 -210
  19. package/src/core/config/loader.ts +117 -97
  20. package/src/core/config/schema.ts +39 -39
  21. package/src/core/database/client.ts +44 -44
  22. package/src/core/database/repositories/dependency.repository.ts +552 -349
  23. package/src/core/database/repositories/domain.repository.ts +275 -198
  24. package/src/core/database/repositories/file.repository.ts +117 -106
  25. package/src/core/database/repositories/meta.repository.ts +33 -33
  26. package/src/core/database/repositories/symbol.repository.ts +482 -443
  27. package/src/core/database/repository.ts +281 -260
  28. package/src/core/database/schema.ts +154 -153
  29. package/src/core/discovery/topology-detector.ts +486 -486
  30. package/src/core/ingestion/hasher.ts +5 -5
  31. package/src/core/ingestion/pipeline.ts +606 -535
  32. package/src/core/parser/boundary-tracker.ts +433 -288
  33. package/src/core/parser/extractors/base.ts +62 -62
  34. package/src/core/parser/extractors/blade.ts +158 -158
  35. package/src/core/parser/extractors/frontend.ts +147 -194
  36. package/src/core/parser/extractors/go.ts +408 -180
  37. package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
  38. package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
  39. package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
  40. package/src/core/parser/extractors/php.ts +415 -153
  41. package/src/core/parser/extractors/python.ts +460 -228
  42. package/src/core/parser/extractors/registry.ts +70 -70
  43. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
  44. package/src/core/parser/extractors/typescript.ts +668 -181
  45. package/src/core/parser/tree-sitter.ts +41 -41
  46. package/src/core/parser/wasm-loader.ts +271 -0
  47. package/src/core/resolver/call-graph-tracer.ts +698 -668
  48. package/src/core/resolver/module-resolver.ts +479 -342
  49. package/src/core/resolver/path-resolver.ts +147 -147
  50. package/src/core/resolver/symbol-locator.ts +543 -543
  51. package/src/core/resolver/vertical-slice-tracer.ts +323 -285
  52. package/src/core/session/session-manager.ts +56 -56
  53. package/src/core/telemetry/telemetry.ts +86 -86
  54. package/src/core/topology/environment-detector.ts +109 -6
  55. package/src/index.ts +21 -21
  56. package/src/mcp/schemas.ts +695 -286
  57. package/src/mcp/server.ts +927 -716
  58. package/src/mcp/tools/check-boundary.ts +307 -152
  59. package/src/mcp/tools/get-domain-catalog.ts +155 -121
  60. package/src/mcp/tools/get-environment-topology.ts +3 -6
  61. package/src/mcp/tools/get-feature-context.ts +227 -116
  62. package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
  63. package/src/mcp/tools/get-symbol-impact.ts +28 -26
  64. package/src/mcp/tools/get-symbol.ts +65 -63
  65. package/src/mcp/tools/locate-symbol.ts +169 -190
  66. package/src/mcp/tools/register-domain.ts +59 -64
  67. package/src/mcp/tools/register-feature.ts +91 -0
  68. package/src/mcp/tools/trace-vertical-slice.ts +74 -48
  69. package/src/types/index.ts +366 -315
  70. package/src/version.ts +3 -3
@@ -1,210 +1,429 @@
1
- import type { BoundaryViolation } from "../../types/index.ts";
2
- import type { ValidatedSeptumConfig } from "../config/schema.ts";
3
- import { ConfigLoader } from "../config/loader.ts";
4
- import type { SeptumRepository } from "../database/repository.ts";
5
- import { ModuleResolver } from "../resolver/module-resolver.ts";
6
-
7
- export class BoundaryEvaluator {
8
- private repo: SeptumRepository;
9
- private resolver: ModuleResolver;
10
-
11
- constructor(repo: SeptumRepository, resolver?: ModuleResolver) {
12
- this.repo = repo;
13
- this.resolver = resolver ?? new ModuleResolver(process.cwd(), repo);
14
- }
15
-
16
- public getResolver(): ModuleResolver {
17
- return this.resolver;
18
- }
19
-
20
- public evaluate(
21
- filePath: string,
22
- proposedImports: string[] = [],
23
- featureKey?: string,
24
- config?: ValidatedSeptumConfig
25
- ): BoundaryViolation[] {
26
- const activeConfig = config ?? ConfigLoader.loadFromDatabaseOrDefaults();
27
- const violations: BoundaryViolation[] = [];
28
-
29
- if (featureKey) {
30
- violations.push(...this.checkFeatureTouchpoints(featureKey, filePath, activeConfig));
31
- }
32
-
33
- if (proposedImports.length > 0) {
34
- violations.push(...this.checkProposedChanges(filePath, proposedImports, activeConfig));
35
- }
36
-
37
- return violations;
38
- }
39
-
40
- public auditCodebase(config: ValidatedSeptumConfig): BoundaryViolation[] {
41
- const violations: BoundaryViolation[] = [];
42
- const allDependencies = this.repo.getAllDependenciesWithDomains();
43
-
44
- for (const dep of allDependencies) {
45
- const targetDomain = this.resolveTargetDomain(dep.target, config, dep.source_file);
46
- if (!targetDomain || targetDomain === dep.source_domain) {
47
- continue;
48
- }
49
-
50
- const sourceDomainConfig = config.domains[dep.source_domain];
51
- if (!sourceDomainConfig) {
52
- continue;
53
- }
54
-
55
- const forbidden = sourceDomainConfig.forbidden_dependencies ?? [];
56
- const allowed = sourceDomainConfig.allowed_dependencies ?? [];
57
-
58
- // 1. Explicit forbidden check
59
- if (forbidden.includes(targetDomain)) {
60
- violations.push({
61
- file: dep.source_file,
62
- line: dep.line_number,
63
- source_domain: dep.source_domain,
64
- target_domain: targetDomain,
65
- imported_target: dep.target,
66
- rule: "forbidden_dependency",
67
- message: `Forbidden cross-domain dependency detected: '${dep.source_domain}' is strictly prohibited from importing domain '${targetDomain}'.`,
68
- });
69
- continue;
70
- }
71
-
72
- // 2. Strict enforcement allowed check
73
- if (config.settings.enforcement === "strict" && !allowed.includes(targetDomain)) {
74
- violations.push({
75
- file: dep.source_file,
76
- line: dep.line_number,
77
- source_domain: dep.source_domain,
78
- target_domain: targetDomain,
79
- imported_target: dep.target,
80
- rule: "disallowed_dependency",
81
- message: `Disallowed dependency: '${targetDomain}' is not declared in allowed_dependencies for domain '${dep.source_domain}'.`,
82
- });
83
- }
84
- }
85
-
86
- return violations;
87
- }
88
-
89
- public checkProposedChanges(
90
- sourceFilePath: string,
91
- proposedImports: string[],
92
- config: ValidatedSeptumConfig
93
- ): BoundaryViolation[] {
94
- const violations: BoundaryViolation[] = [];
95
- const sourceDomain = this.resolveSourceDomain(sourceFilePath, config);
96
-
97
- if (!sourceDomain) {
98
- return violations;
99
- }
100
-
101
- const sourceDomainConfig = config.domains[sourceDomain];
102
- if (!sourceDomainConfig) {
103
- return violations;
104
- }
105
-
106
- const forbidden = sourceDomainConfig.forbidden_dependencies ?? [];
107
- const allowed = sourceDomainConfig.allowed_dependencies ?? [];
108
-
109
- for (const statement of proposedImports) {
110
- const targetDomain = this.resolveTargetDomain(statement, config, sourceFilePath);
111
- if (!targetDomain || targetDomain === sourceDomain) {
112
- continue;
113
- }
114
-
115
- if (forbidden.includes(targetDomain)) {
116
- violations.push({
117
- file: sourceFilePath,
118
- line: 1,
119
- source_domain: sourceDomain,
120
- target_domain: targetDomain,
121
- imported_target: statement,
122
- rule: "forbidden_dependency",
123
- message: `Illegal import blocked: Domain '${sourceDomain}' is strictly forbidden from importing domain '${targetDomain}'.`,
124
- });
125
- } else if (config.settings.enforcement === "strict" && !allowed.includes(targetDomain)) {
126
- violations.push({
127
- file: sourceFilePath,
128
- line: 1,
129
- source_domain: sourceDomain,
130
- target_domain: targetDomain,
131
- imported_target: statement,
132
- rule: "disallowed_dependency",
133
- message: `Unlisted domain dependency: '${targetDomain}' must be explicitly added to allowed_dependencies before domain '${sourceDomain}' can import it.`,
134
- });
135
- }
136
- }
137
-
138
- return violations;
139
- }
140
-
141
- public resolveSourceDomain(filePath: string, config: ValidatedSeptumConfig): string | null {
142
- return this.resolver.resolveSourceDomain(filePath, config);
143
- }
144
-
145
- public resolveTargetDomain(
146
- targetStatement: string,
147
- config: ValidatedSeptumConfig,
148
- sourceFilePath?: string
149
- ): string | null {
150
- return this.resolver.resolveTargetDomain(targetStatement, sourceFilePath, config);
151
- }
152
-
153
- public checkFeatureTouchpoints(
154
- featureKey: string,
155
- targetFilePath: string,
156
- config: ValidatedSeptumConfig
157
- ): BoundaryViolation[] {
158
- const violations: BoundaryViolation[] = [];
159
- const features = config.features ?? {};
160
- const featureConfig = features[featureKey];
161
-
162
- if (!featureConfig) {
163
- violations.push({
164
- file: targetFilePath,
165
- line: 1,
166
- source_domain: "unknown",
167
- target_domain: "unknown",
168
- imported_target: targetFilePath,
169
- rule: "unknown_feature",
170
- message: `Feature '${featureKey}' is not defined in septum configuration.`,
171
- });
172
- return violations;
173
- }
174
-
175
- const normalizedTarget = targetFilePath.replace(/\\/g, "/").replace(/^\.\//, "");
176
- const allowedTouchpoints = featureConfig.allowed_touchpoints.map((tp) =>
177
- tp.replace(/\\/g, "/").replace(/^\.\//, "")
178
- );
179
-
180
- const isAllowed = allowedTouchpoints.some((pattern) => {
181
- if (pattern.endsWith("/**")) {
182
- const prefix = pattern.slice(0, -3);
183
- return normalizedTarget.startsWith(prefix);
184
- }
185
- if (pattern.endsWith("/*")) {
186
- const prefix = pattern.slice(0, -2);
187
- return normalizedTarget.startsWith(prefix);
188
- }
189
- return (
190
- normalizedTarget === pattern ||
191
- normalizedTarget.endsWith("/" + pattern) ||
192
- pattern.endsWith("/" + normalizedTarget)
193
- );
194
- });
195
-
196
- if (!isAllowed) {
197
- violations.push({
198
- file: targetFilePath,
199
- line: 1,
200
- source_domain: featureConfig.domain,
201
- target_domain: "unknown",
202
- imported_target: targetFilePath,
203
- rule: "touchpoint_violation",
204
- message: `Scope violation: File '${targetFilePath}' is outside the declared allowed_touchpoints for feature '${featureKey}'. Permitted: [${allowedTouchpoints.join(", ")}].`,
205
- });
206
- }
207
-
208
- return violations;
209
- }
210
- }
1
+ import { posix } from "node:path";
2
+ import type { BoundaryViolation } from "../../types/index.ts";
3
+ import type { ValidatedSeptumConfig } from "../config/schema.ts";
4
+ import { ConfigLoader } from "../config/loader.ts";
5
+ import type { SeptumRepository } from "../database/repository.ts";
6
+ import { ModuleResolver } from "../resolver/module-resolver.ts";
7
+
8
+ export class BoundaryEvaluator {
9
+ private repo: SeptumRepository;
10
+ private resolver: ModuleResolver;
11
+
12
+ constructor(repo: SeptumRepository, resolver?: ModuleResolver) {
13
+ this.repo = repo;
14
+ this.resolver = resolver ?? new ModuleResolver(process.cwd(), repo);
15
+ }
16
+
17
+ public getResolver(): ModuleResolver {
18
+ return this.resolver;
19
+ }
20
+
21
+ public evaluate(
22
+ filePath: string,
23
+ proposedImports: string[] = [],
24
+ featureKey?: string,
25
+ config?: ValidatedSeptumConfig
26
+ ): BoundaryViolation[] {
27
+ let activeConfig = config;
28
+ if (!activeConfig) {
29
+ const fromRepo = ConfigLoader.loadFromRepository(this.repo);
30
+ activeConfig =
31
+ Object.keys(fromRepo.domains).length > 0
32
+ ? fromRepo
33
+ : ConfigLoader.loadFromDatabaseOrDefaults();
34
+ }
35
+ const violations: BoundaryViolation[] = [];
36
+
37
+ if (featureKey) {
38
+ violations.push(...this.checkFeatureTouchpoints(featureKey, filePath, activeConfig));
39
+ }
40
+
41
+ if (proposedImports.length > 0) {
42
+ violations.push(...this.checkProposedChanges(filePath, proposedImports, activeConfig));
43
+ }
44
+
45
+ return violations;
46
+ }
47
+
48
+ public auditCodebase(config: ValidatedSeptumConfig): BoundaryViolation[] {
49
+ const violations: BoundaryViolation[] = [];
50
+ const allDependencies = this.repo.getAllDependenciesWithDomains();
51
+
52
+ for (const dep of allDependencies) {
53
+ const targetDomain = this.resolveTargetDomain(dep.target, config, dep.source_file);
54
+ if (!targetDomain || targetDomain === dep.source_domain) {
55
+ continue;
56
+ }
57
+
58
+ const sourceDomainConfig = config.domains[dep.source_domain];
59
+ if (!sourceDomainConfig) {
60
+ continue;
61
+ }
62
+
63
+ const forbidden = sourceDomainConfig.forbidden_dependencies ?? [];
64
+ const allowed = sourceDomainConfig.allowed_dependencies ?? [];
65
+
66
+ // 1. Explicit forbidden check
67
+ if (forbidden.includes(targetDomain)) {
68
+ violations.push({
69
+ file: dep.source_file,
70
+ line: dep.line_number,
71
+ source_domain: dep.source_domain,
72
+ target_domain: targetDomain,
73
+ imported_target: dep.target,
74
+ rule: "forbidden_dependency",
75
+ message: `Forbidden cross-domain dependency detected: '${dep.source_domain}' is strictly prohibited from importing domain '${targetDomain}'.`,
76
+ });
77
+ continue;
78
+ }
79
+
80
+ // 2. Strict enforcement allowed check
81
+ if (config.settings.enforcement === "strict" && !allowed.includes(targetDomain)) {
82
+ violations.push({
83
+ file: dep.source_file,
84
+ line: dep.line_number,
85
+ source_domain: dep.source_domain,
86
+ target_domain: targetDomain,
87
+ imported_target: dep.target,
88
+ rule: "disallowed_dependency",
89
+ message: `Disallowed dependency: '${targetDomain}' is not declared in allowed_dependencies for domain '${dep.source_domain}'.`,
90
+ });
91
+ }
92
+ }
93
+
94
+ violations.push(...this.detectCircularDependencies(config, allDependencies));
95
+
96
+ return violations;
97
+ }
98
+
99
+ public detectCircularDependencies(
100
+ config: ValidatedSeptumConfig,
101
+ preloadedDeps?: Array<{
102
+ source_file: string;
103
+ source_domain: string;
104
+ line_number: number;
105
+ target: string;
106
+ statement: string;
107
+ is_external: boolean;
108
+ }>
109
+ ): BoundaryViolation[] {
110
+ const violations: BoundaryViolation[] = [];
111
+ const allDependencies = preloadedDeps ?? this.repo.getAllDependenciesWithDomains();
112
+
113
+ // Prepare file lookup maps for internal file-to-file resolution
114
+ const fileByNormPath = new Map<string, string>();
115
+ const fileByNoExtPath = new Map<string, string>();
116
+
117
+ const registerKnownFile = (rawPath: string) => {
118
+ const norm = rawPath.replace(/\\/g, "/").replace(/^\.\//, "");
119
+ fileByNormPath.set(norm.toLowerCase(), rawPath);
120
+ const noExt = norm.replace(/\.[^/.]+$/, "").toLowerCase();
121
+ fileByNoExtPath.set(noExt, rawPath);
122
+ if (noExt.endsWith("/index")) {
123
+ fileByNoExtPath.set(noExt.slice(0, -"/index".length), rawPath);
124
+ }
125
+ };
126
+
127
+ if (typeof this.repo.getAllFiles === "function") {
128
+ for (const f of this.repo.getAllFiles()) {
129
+ registerKnownFile(f.path);
130
+ }
131
+ }
132
+ for (const d of allDependencies) {
133
+ registerKnownFile(d.source_file);
134
+ }
135
+
136
+ const resolveTargetFile = (sourceFile: string, rawTarget: string): string | null => {
137
+ const cleaned = rawTarget.trim().replace(/^['"]|['"]$/g, "").replace(/\\/g, "/");
138
+ if (!cleaned) return null;
139
+
140
+ if (cleaned.startsWith(".")) {
141
+ const srcDir = posix.dirname(sourceFile.replace(/\\/g, "/"));
142
+ const joined = posix.normalize(posix.join(srcDir, cleaned)).replace(/^\.\//, "");
143
+ const lowerJoined = joined.toLowerCase();
144
+ const exact =
145
+ fileByNormPath.get(lowerJoined) ??
146
+ fileByNoExtPath.get(lowerJoined.replace(/\.[^/.]+$/, ""));
147
+ if (exact) return exact;
148
+ }
149
+
150
+ const aliasClean = cleaned.replace(/^@\//, "src/").replace(/^\.\//, "");
151
+ const lowerAlias = aliasClean.toLowerCase();
152
+ const byAlias =
153
+ fileByNormPath.get(lowerAlias) ??
154
+ fileByNoExtPath.get(lowerAlias.replace(/\.[^/.]+$/, ""));
155
+ if (byAlias) return byAlias;
156
+
157
+ return null;
158
+ };
159
+
160
+ type EdgeMeta = {
161
+ file: string;
162
+ line: number;
163
+ source_domain: string;
164
+ target_domain: string;
165
+ target: string;
166
+ };
167
+
168
+ const fileGraph = new Map<string, Map<string, EdgeMeta>>();
169
+ const domainGraph = new Map<string, Map<string, EdgeMeta>>();
170
+
171
+ for (const dep of allDependencies) {
172
+ if (dep.is_external) continue;
173
+
174
+ const targetDomain = this.resolveTargetDomain(dep.target, config, dep.source_file);
175
+
176
+ // 1. Domain-to-domain edge
177
+ if (targetDomain && targetDomain !== dep.source_domain) {
178
+ let outMap = domainGraph.get(dep.source_domain);
179
+ if (!outMap) {
180
+ outMap = new Map();
181
+ domainGraph.set(dep.source_domain, outMap);
182
+ }
183
+ if (!outMap.has(targetDomain)) {
184
+ outMap.set(targetDomain, {
185
+ file: dep.source_file,
186
+ line: dep.line_number,
187
+ source_domain: dep.source_domain,
188
+ target_domain: targetDomain,
189
+ target: dep.target,
190
+ });
191
+ }
192
+ }
193
+
194
+ // 2. File-to-file edge
195
+ const targetFile = resolveTargetFile(dep.source_file, dep.target);
196
+ if (targetFile && targetFile !== dep.source_file) {
197
+ let outMap = fileGraph.get(dep.source_file);
198
+ if (!outMap) {
199
+ outMap = new Map();
200
+ fileGraph.set(dep.source_file, outMap);
201
+ }
202
+ if (!outMap.has(targetFile)) {
203
+ outMap.set(targetFile, {
204
+ file: dep.source_file,
205
+ line: dep.line_number,
206
+ source_domain: dep.source_domain,
207
+ target_domain: targetDomain ?? dep.source_domain,
208
+ target: dep.target,
209
+ });
210
+ }
211
+ }
212
+ }
213
+
214
+ const findCycles = (
215
+ graph: Map<string, Map<string, EdgeMeta>>,
216
+ kindLabel: "file" | "domain"
217
+ ) => {
218
+ const visited = new Set<string>();
219
+ const inStack = new Set<string>();
220
+ const stack: string[] = [];
221
+ const reportedCycles = new Set<string>();
222
+
223
+ const canonicalizeCycle = (nodes: string[]): string => {
224
+ if (nodes.length === 0) return "";
225
+ let minIdx = 0;
226
+ for (let i = 1; i < nodes.length; i++) {
227
+ if (nodes[i] < nodes[minIdx]) minIdx = i;
228
+ }
229
+ const rotated = [...nodes.slice(minIdx), ...nodes.slice(0, minIdx)];
230
+ return rotated.join("->");
231
+ };
232
+
233
+ const dfs = (node: string) => {
234
+ visited.add(node);
235
+ inStack.add(node);
236
+ stack.push(node);
237
+
238
+ const neighbors = graph.get(node);
239
+ if (neighbors) {
240
+ for (const [nextNode, edgeMeta] of neighbors.entries()) {
241
+ if (!visited.has(nextNode)) {
242
+ dfs(nextNode);
243
+ } else if (inStack.has(nextNode)) {
244
+ const cycleStartIndex = stack.indexOf(nextNode);
245
+ if (cycleStartIndex !== -1) {
246
+ const cycleNodes = stack.slice(cycleStartIndex);
247
+ const sig = canonicalizeCycle(cycleNodes);
248
+ if (!reportedCycles.has(sig)) {
249
+ reportedCycles.add(sig);
250
+ const firstNode = cycleNodes[0];
251
+ const secondNode = cycleNodes[1] ?? nextNode;
252
+ const firstEdge = graph.get(firstNode)?.get(secondNode) ?? edgeMeta;
253
+ const cycleDisplay = [...cycleNodes, cycleNodes[0]].join(" -> ");
254
+ violations.push({
255
+ file: firstEdge.file,
256
+ line: firstEdge.line,
257
+ source_domain: firstEdge.source_domain,
258
+ target_domain: firstEdge.target_domain,
259
+ imported_target: firstEdge.target,
260
+ rule: "circular_dependency",
261
+ message: `Circular ${kindLabel} dependency detected: ${cycleDisplay}.`,
262
+ });
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ stack.pop();
270
+ inStack.delete(node);
271
+ };
272
+
273
+ for (const startNode of graph.keys()) {
274
+ if (!visited.has(startNode)) {
275
+ dfs(startNode);
276
+ }
277
+ }
278
+ };
279
+
280
+ findCycles(fileGraph, "file");
281
+ findCycles(domainGraph, "domain");
282
+
283
+ return violations;
284
+ }
285
+
286
+ public checkProposedChanges(
287
+ sourceFilePath: string,
288
+ proposedImports: string[],
289
+ config: ValidatedSeptumConfig
290
+ ): BoundaryViolation[] {
291
+ const violations: BoundaryViolation[] = [];
292
+ const sourceDomain = this.resolveSourceDomain(sourceFilePath, config);
293
+
294
+ if (!sourceDomain) {
295
+ violations.push({
296
+ file: sourceFilePath,
297
+ line: 1,
298
+ source_domain: "unmapped",
299
+ target_domain: "unknown",
300
+ imported_target: "",
301
+ rule: "unmapped_domain",
302
+ message: `File '${sourceFilePath}' does not belong to any declared bounded context. Boundary integrity cannot be enforced.`,
303
+ });
304
+ return violations;
305
+ }
306
+
307
+ const sourceDomainConfig = config.domains[sourceDomain];
308
+ if (!sourceDomainConfig) {
309
+ return violations;
310
+ }
311
+
312
+ const forbidden = sourceDomainConfig.forbidden_dependencies ?? [];
313
+ const allowed = sourceDomainConfig.allowed_dependencies ?? [];
314
+
315
+ for (const statement of proposedImports) {
316
+ const targetDomain = this.resolveTargetDomain(statement, config, sourceFilePath);
317
+ if (!targetDomain || targetDomain === sourceDomain) {
318
+ continue;
319
+ }
320
+
321
+ if (forbidden.includes(targetDomain)) {
322
+ violations.push({
323
+ file: sourceFilePath,
324
+ line: 1,
325
+ source_domain: sourceDomain,
326
+ target_domain: targetDomain,
327
+ imported_target: statement,
328
+ rule: "forbidden_dependency",
329
+ message: `Illegal import blocked: Domain '${sourceDomain}' is strictly forbidden from importing domain '${targetDomain}'.`,
330
+ });
331
+ } else if (config.settings.enforcement === "strict" && !allowed.includes(targetDomain)) {
332
+ violations.push({
333
+ file: sourceFilePath,
334
+ line: 1,
335
+ source_domain: sourceDomain,
336
+ target_domain: targetDomain,
337
+ imported_target: statement,
338
+ rule: "disallowed_dependency",
339
+ message: `Unlisted domain dependency: '${targetDomain}' must be explicitly added to allowed_dependencies before domain '${sourceDomain}' can import it.`,
340
+ });
341
+ }
342
+ }
343
+
344
+ return violations;
345
+ }
346
+
347
+ public resolveSourceDomain(filePath: string, config: ValidatedSeptumConfig): string | null {
348
+ return this.resolver.resolveSourceDomain(filePath, config);
349
+ }
350
+
351
+ public resolveTargetDomain(
352
+ targetStatement: string,
353
+ config: ValidatedSeptumConfig,
354
+ sourceFilePath?: string
355
+ ): string | null {
356
+ return this.resolver.resolveTargetDomain(targetStatement, sourceFilePath, config);
357
+ }
358
+
359
+ public checkFeatureTouchpoints(
360
+ featureKey: string,
361
+ targetFilePath: string,
362
+ config: ValidatedSeptumConfig
363
+ ): BoundaryViolation[] {
364
+ const violations: BoundaryViolation[] = [];
365
+ const features = config.features ?? {};
366
+ const featureConfig = features[featureKey];
367
+
368
+ if (!featureConfig) {
369
+ violations.push({
370
+ file: targetFilePath,
371
+ line: 1,
372
+ source_domain: "unknown",
373
+ target_domain: "unknown",
374
+ imported_target: targetFilePath,
375
+ rule: "unknown_feature",
376
+ message: `Feature '${featureKey}' is not defined in septum configuration.`,
377
+ });
378
+ return violations;
379
+ }
380
+
381
+ const normalizedTarget = targetFilePath.replace(/\\/g, "/").replace(/^\.\//, "");
382
+ const allowedTouchpoints = featureConfig.allowed_touchpoints.map((tp) =>
383
+ tp.replace(/\\/g, "/").replace(/^\.\//, "")
384
+ );
385
+
386
+ const domainCfg = config.domains[featureConfig.domain];
387
+ const domainRoot = domainCfg?.root ? domainCfg.root.replace(/\\/g, "/").replace(/^\.\//, "") : "";
388
+
389
+ const isAllowed = allowedTouchpoints.some((pattern) => {
390
+ if (pattern.endsWith("/**")) {
391
+ const prefix = pattern.slice(0, -3);
392
+ return normalizedTarget.startsWith(prefix);
393
+ }
394
+ if (pattern.endsWith("/*")) {
395
+ const prefix = pattern.slice(0, -2);
396
+ return normalizedTarget.startsWith(prefix);
397
+ }
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
+ });
414
+
415
+ if (!isAllowed) {
416
+ violations.push({
417
+ file: targetFilePath,
418
+ line: 1,
419
+ source_domain: featureConfig.domain,
420
+ target_domain: "unknown",
421
+ imported_target: targetFilePath,
422
+ rule: "touchpoint_violation",
423
+ message: `Scope violation: File '${targetFilePath}' is outside the declared allowed_touchpoints for feature '${featureKey}'. Permitted: [${allowedTouchpoints.join(", ")}].`,
424
+ });
425
+ }
426
+
427
+ return violations;
428
+ }
429
+ }