archforge-x 1.0.7 → 2.0.0-beta.1

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.
@@ -9,6 +9,11 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const typescript_1 = __importDefault(require("typescript"));
11
11
  const chalk_1 = __importDefault(require("chalk"));
12
+ const minimatch_1 = require("minimatch");
13
+ // Ignore comment patterns
14
+ const IGNORE_FILE_PATTERN = /@archforge-ignore-file/;
15
+ const IGNORE_LINE_PATTERN = /@archforge-ignore-line/;
16
+ const IGNORE_NEXT_LINE_PATTERN = /@archforge-ignore-next-line/;
12
17
  // --- LANGUAGE PARSERS ---
13
18
  const parseImportsTS = (content, filePath) => {
14
19
  const imports = [];
@@ -16,21 +21,74 @@ const parseImportsTS = (content, filePath) => {
16
21
  sourceFile.forEachChild((node) => {
17
22
  if (typescript_1.default.isImportDeclaration(node) && node.moduleSpecifier) {
18
23
  const importPath = node.moduleSpecifier.text;
19
- imports.push(importPath);
24
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
25
+ imports.push({
26
+ path: importPath,
27
+ line: line + 1, // Convert to 1-based
28
+ column: character + 1
29
+ });
20
30
  }
21
31
  if (typescript_1.default.isVariableStatement(node)) {
22
32
  const text = node.getText();
23
33
  const match = text.match(/require\(['"](.+)['"]\)/);
24
- if (match)
25
- imports.push(match[1]);
34
+ if (match) {
35
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
36
+ imports.push({
37
+ path: match[1],
38
+ line: line + 1,
39
+ column: character + 1
40
+ });
41
+ }
26
42
  }
27
43
  });
28
44
  return imports;
29
45
  };
46
+ /**
47
+ * Check if a file should be completely ignored
48
+ */
49
+ function shouldIgnoreFile(content, filePath, arch) {
50
+ // Check for @archforge-ignore-file comment
51
+ if (IGNORE_FILE_PATTERN.test(content)) {
52
+ const match = content.match(/@archforge-ignore-file(?:\s*:\s*(.+))?/);
53
+ return { ignored: true, reason: match?.[1]?.trim() || 'File-level ignore' };
54
+ }
55
+ // Check against config ignore patterns
56
+ if (arch.ignore?.files?.includes(filePath)) {
57
+ return { ignored: true, reason: 'Listed in ignore.files' };
58
+ }
59
+ if (arch.ignore?.patterns) {
60
+ for (const pattern of arch.ignore.patterns) {
61
+ if ((0, minimatch_1.minimatch)(filePath, pattern)) {
62
+ return { ignored: true, reason: `Matches ignore pattern: ${pattern}` };
63
+ }
64
+ }
65
+ }
66
+ return { ignored: false };
67
+ }
68
+ /**
69
+ * Check if a specific line should be ignored
70
+ */
71
+ function shouldIgnoreLine(lines, lineNumber) {
72
+ const lineIndex = lineNumber - 1;
73
+ const currentLine = lines[lineIndex] || '';
74
+ const previousLine = lines[lineIndex - 1] || '';
75
+ // Check current line for inline ignore
76
+ if (IGNORE_LINE_PATTERN.test(currentLine)) {
77
+ const match = currentLine.match(/@archforge-ignore-line(?:\s*:\s*(.+))?/);
78
+ return { ignored: true, reason: match?.[1]?.trim() || 'Inline ignore' };
79
+ }
80
+ // Check previous line for next-line ignore
81
+ if (IGNORE_NEXT_LINE_PATTERN.test(previousLine)) {
82
+ const match = previousLine.match(/@archforge-ignore-next-line(?:\s*:\s*(.+))?/);
83
+ return { ignored: true, reason: match?.[1]?.trim() || 'Next-line ignore' };
84
+ }
85
+ return { ignored: false };
86
+ }
30
87
  // --- CORE ANALYZER ---
31
88
  function analyzeDependencies(arch, options = {}) {
32
89
  const violations = [];
33
90
  const projectRoot = path_1.default.resolve(arch.project.root || ".");
91
+ const forbiddenPackages = arch.rules?.imports?.forbiddenPackages || [];
34
92
  function getLayerByPath(filePath) {
35
93
  const absoluteFile = path_1.default.resolve(filePath);
36
94
  const sortedLayers = [...arch.layers].sort((a, b) => b.path.length - a.path.length);
@@ -85,37 +143,145 @@ function analyzeDependencies(arch, options = {}) {
85
143
  if (!currentLayer)
86
144
  return;
87
145
  const content = fs_extra_1.default.readFileSync(filePath, "utf-8");
146
+ const relativeFilePath = path_1.default.relative(projectRoot, filePath);
147
+ const lines = content.split('\n');
148
+ // Check if entire file should be ignored
149
+ const fileIgnore = shouldIgnoreFile(content, relativeFilePath, arch);
150
+ if (fileIgnore.ignored && !options.includeIgnored) {
151
+ return;
152
+ }
88
153
  let imports = [];
89
- if ([".ts", ".tsx", ".js", ".jsx"].includes(ext))
154
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
90
155
  imports = parseImportsTS(content, filePath);
91
- for (const importStr of imports) {
92
- const importedLayer = resolveImportedLayer(filePath, importStr);
156
+ }
157
+ for (const importInfo of imports) {
158
+ // Check for line-level ignore
159
+ const lineIgnore = shouldIgnoreLine(lines, importInfo.line);
160
+ const isIgnored = fileIgnore.ignored || lineIgnore.ignored;
161
+ const ignoreReason = fileIgnore.reason || lineIgnore.reason;
162
+ // Check forbidden packages first
163
+ const packageName = importInfo.path.split('/')[0];
164
+ const forbiddenPkg = forbiddenPackages.find(fp => fp.name === packageName || importInfo.path.startsWith(fp.name));
165
+ if (forbiddenPkg) {
166
+ const violation = {
167
+ file: relativeFilePath,
168
+ line: importInfo.line,
169
+ column: importInfo.column,
170
+ fromLayer: currentLayer.name,
171
+ importedLayer: packageName,
172
+ importPath: importInfo.path,
173
+ type: "forbidden-package",
174
+ severity: forbiddenPkg.severity || "error",
175
+ message: forbiddenPkg.message || `Package '${packageName}' is forbidden`,
176
+ ignored: isIgnored,
177
+ ignoreReason
178
+ };
179
+ if (!isIgnored || options.includeIgnored) {
180
+ violations.push(violation);
181
+ }
182
+ continue;
183
+ }
184
+ // Check layer violations
185
+ const importedLayer = resolveImportedLayer(filePath, importInfo.path);
93
186
  if (!importedLayer || importedLayer.name === currentLayer.name)
94
187
  continue;
95
188
  const canImport = currentLayer.canImport || currentLayer.allowedImports || [];
96
189
  if (!canImport.includes(importedLayer.name)) {
97
- violations.push({
98
- file: path_1.default.relative(projectRoot, filePath),
190
+ const violation = {
191
+ file: relativeFilePath,
192
+ line: importInfo.line,
193
+ column: importInfo.column,
99
194
  fromLayer: currentLayer.name,
100
195
  importedLayer: importedLayer.name,
101
- importPath: importStr,
196
+ importPath: importInfo.path,
102
197
  type: "forbidden",
103
- severity: "error"
104
- });
198
+ severity: "error",
199
+ ignored: isIgnored,
200
+ ignoreReason
201
+ };
202
+ if (!isIgnored || options.includeIgnored) {
203
+ violations.push(violation);
204
+ }
105
205
  }
106
206
  }
107
207
  }
208
+ // Check file location rules
209
+ function checkFileLocations() {
210
+ const locationRules = arch.rules?.fileLocations || [];
211
+ if (locationRules.length === 0)
212
+ return;
213
+ function scanForLocationViolations(folderPath) {
214
+ const absoluteFolderPath = path_1.default.resolve(projectRoot, folderPath);
215
+ if (!fs_extra_1.default.existsSync(absoluteFolderPath))
216
+ return;
217
+ const entries = fs_extra_1.default.readdirSync(absoluteFolderPath);
218
+ for (const entry of entries) {
219
+ const fullPath = path_1.default.join(absoluteFolderPath, entry);
220
+ const stat = fs_extra_1.default.statSync(fullPath);
221
+ if (stat.isDirectory()) {
222
+ if (["node_modules", ".git", "dist"].includes(entry))
223
+ continue;
224
+ scanForLocationViolations(fullPath);
225
+ }
226
+ else if (stat.isFile()) {
227
+ const relativeFilePath = path_1.default.relative(projectRoot, fullPath);
228
+ for (const rule of locationRules) {
229
+ if ((0, minimatch_1.minimatch)(entry, rule.pattern) || (0, minimatch_1.minimatch)(relativeFilePath, rule.pattern)) {
230
+ const isInAllowedPath = rule.allowedPaths.some(allowedPath => (0, minimatch_1.minimatch)(relativeFilePath, allowedPath));
231
+ if (!isInAllowedPath) {
232
+ const content = fs_extra_1.default.readFileSync(fullPath, "utf-8");
233
+ const fileIgnore = shouldIgnoreFile(content, relativeFilePath, arch);
234
+ if (!fileIgnore.ignored || options.includeIgnored) {
235
+ violations.push({
236
+ file: relativeFilePath,
237
+ fromLayer: "unknown",
238
+ importedLayer: "N/A",
239
+ importPath: "N/A",
240
+ type: "file-location",
241
+ severity: rule.severity,
242
+ message: rule.message || `File '${entry}' is not in an allowed location. Expected: ${rule.allowedPaths.join(' or ')}`,
243
+ ignored: fileIgnore.ignored,
244
+ ignoreReason: fileIgnore.reason
245
+ });
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ scanForLocationViolations("src");
254
+ }
108
255
  arch.layers.forEach((layer) => scanFolder(layer.path));
109
- return violations;
256
+ checkFileLocations();
257
+ // Sort violations: errors first, then by file
258
+ return violations.sort((a, b) => {
259
+ if (a.ignored !== b.ignored)
260
+ return a.ignored ? 1 : -1;
261
+ if (a.severity !== b.severity) {
262
+ const severityOrder = { error: 0, warning: 1, info: 2 };
263
+ return severityOrder[a.severity] - severityOrder[b.severity];
264
+ }
265
+ return a.file.localeCompare(b.file);
266
+ });
110
267
  }
111
268
  function reportViolations(violations, options = {}) {
112
- if (violations.length === 0) {
269
+ const activeViolations = violations.filter(v => !v.ignored);
270
+ const ignoredViolations = violations.filter(v => v.ignored);
271
+ if (activeViolations.length === 0) {
113
272
  console.log(chalk_1.default.green("✅ No architectural violations found. Architecture is clean."));
273
+ if (ignoredViolations.length > 0) {
274
+ console.log(chalk_1.default.gray(` (${ignoredViolations.length} violation(s) ignored via @archforge-ignore)`));
275
+ }
114
276
  return;
115
277
  }
116
- console.log(chalk_1.default.red.bold(`❌ Found ${violations.length} Architecture Violations:`));
117
- const grouped = violations.reduce((acc, v) => {
118
- const key = `${v.fromLayer} -> ${v.importedLayer}`;
278
+ console.log(chalk_1.default.red.bold(`\n❌ Found ${activeViolations.length} Architecture Violation(s):`));
279
+ const grouped = activeViolations.reduce((acc, v) => {
280
+ const key = v.type === "forbidden-package"
281
+ ? `forbidden-package:${v.importedLayer}`
282
+ : v.type === "file-location"
283
+ ? `file-location`
284
+ : `${v.fromLayer} -> ${v.importedLayer}`;
119
285
  if (!acc[key])
120
286
  acc[key] = [];
121
287
  acc[key].push(v);
@@ -124,17 +290,40 @@ function reportViolations(violations, options = {}) {
124
290
  Object.keys(grouped).forEach(key => {
125
291
  const group = grouped[key];
126
292
  const v = group[0];
127
- const color = v.severity === "error" ? chalk_1.default.red : chalk_1.default.yellow;
128
- console.log(color.bold(`\n[${v.type.toUpperCase()}] ${v.fromLayer} depends on ${v.importedLayer}`));
293
+ const color = v.severity === "error" ? chalk_1.default.red : v.severity === "warning" ? chalk_1.default.yellow : chalk_1.default.blue;
294
+ const icon = v.severity === "error" ? "❌" : v.severity === "warning" ? "⚠️" : "ℹ️";
295
+ if (v.type === "forbidden-package") {
296
+ console.log(color.bold(`\n${icon} [FORBIDDEN PACKAGE] ${v.importedLayer}`));
297
+ }
298
+ else if (v.type === "file-location") {
299
+ console.log(color.bold(`\n${icon} [FILE LOCATION] Misplaced files`));
300
+ }
301
+ else {
302
+ console.log(color.bold(`\n${icon} [${v.type.toUpperCase()}] ${v.fromLayer} depends on ${v.importedLayer}`));
303
+ }
129
304
  group.forEach(item => {
130
- console.log(chalk_1.default.gray(` - ${item.file} imports '${item.importPath}'`));
305
+ const location = item.line ? `:${item.line}:${item.column || 1}` : '';
306
+ console.log(chalk_1.default.gray(` • ${item.file}${location}`));
307
+ if (item.message) {
308
+ console.log(chalk_1.default.gray(` ${item.message}`));
309
+ }
310
+ else {
311
+ console.log(chalk_1.default.gray(` imports '${item.importPath}'`));
312
+ }
131
313
  });
132
314
  });
315
+ if (ignoredViolations.length > 0) {
316
+ console.log(chalk_1.default.gray(`\n📝 ${ignoredViolations.length} violation(s) ignored via @archforge-ignore`));
317
+ }
133
318
  if (options.outputFile) {
134
319
  const absoluteOutput = path_1.default.resolve(options.outputFile);
135
320
  const ext = path_1.default.extname(absoluteOutput).toLowerCase();
136
321
  const content = ext === ".json" ? JSON.stringify(violations, null, 2) :
137
- violations.map(v => `[${v.severity.toUpperCase()}] ${v.fromLayer} -> ${v.importedLayer} in ${v.file}`).join("\n");
322
+ violations.map(v => {
323
+ const status = v.ignored ? '[IGNORED] ' : '';
324
+ const location = v.line ? `:${v.line}` : '';
325
+ return `${status}[${v.severity.toUpperCase()}] ${v.fromLayer} -> ${v.importedLayer} in ${v.file}${location}`;
326
+ }).join("\n");
138
327
  fs_extra_1.default.writeFileSync(absoluteOutput, content, "utf-8");
139
328
  console.log(chalk_1.default.blueBright(`\n📄 Detailed report saved to ${absoluteOutput}`));
140
329
  }
@@ -27,14 +27,20 @@ async function interactiveCLI() {
27
27
  { title: "JavaScript", value: "js" },
28
28
  ],
29
29
  });
30
+ // NestJS only supports TypeScript
31
+ const frameworkChoices = langResponse.language === "ts"
32
+ ? [
33
+ { title: "NestJS", value: "nestjs" },
34
+ { title: "Express", value: "express" },
35
+ ]
36
+ : [
37
+ { title: "Express", value: "express" },
38
+ ];
30
39
  const frameworkResponse = await (0, prompts_1.default)({
31
40
  type: "select",
32
41
  name: "framework",
33
42
  message: "Select framework:",
34
- choices: [
35
- { title: "NestJS", value: "nestjs" },
36
- { title: "Express", value: "express" },
37
- ],
43
+ choices: frameworkChoices,
38
44
  });
39
45
  const archResponse = await (0, prompts_1.default)({
40
46
  type: "select",
@@ -13,6 +13,16 @@ exports.NodePackageVersions = {
13
13
  uuid: "9.0.1",
14
14
  zod: "3.23.8",
15
15
  "http-status-codes": "2.3.0",
16
+ // Authentication
17
+ jsonwebtoken: "^9.0.2",
18
+ bcrypt: "^5.1.1",
19
+ "@types/jsonwebtoken": "^9.0.6",
20
+ "@types/bcrypt": "^5.0.2",
21
+ "passport-jwt": "^4.0.1",
22
+ "@types/passport-jwt": "^4.0.1",
23
+ "@nestjs/jwt": "^10.2.0",
24
+ "@nestjs/passport": "^10.0.3",
25
+ passport: "^0.7.0",
16
26
  // NestJS (v11)
17
27
  "@nestjs/common": "^11.0.0",
18
28
  "@nestjs/core": "^11.0.0",
@@ -41,6 +41,12 @@ function mergeArchitectures(base, child) {
41
41
  project: { ...base.project, ...child.project },
42
42
  metadata: { ...base.metadata, ...child.metadata },
43
43
  layers: Array.from(layerMap.values()),
44
+ rules: { ...base.rules, ...child.rules },
45
+ naming: { ...base.naming, ...child.naming },
46
+ ignore: {
47
+ files: [...(base.ignore?.files || []), ...(child.ignore?.files || [])],
48
+ patterns: [...(base.ignore?.patterns || []), ...(child.ignore?.patterns || [])],
49
+ },
44
50
  };
45
51
  }
46
52
  // --- MAIN FUNCTIONS ---
@@ -66,11 +72,27 @@ function loadArchitecture(filePath) {
66
72
  }
67
73
  }
68
74
  catch (e) {
69
- throw new Error(`Syntax error in ${filePath}: ${e.message}`);
75
+ throw new Error(`Syntax error in ${filePath}:\n` +
76
+ `${e.message}\n\n` +
77
+ chalk_1.default.yellow(`💡 Tip: Validate your YAML/JSON syntax at https://jsonlint.com or https://yamlvalidator.com`));
70
78
  }
71
79
  // 3. Handle Inheritance (Extends)
72
80
  if (parsed.extends) {
73
- const parentPath = path_1.default.resolve(path_1.default.dirname(absolutePath), parsed.extends);
81
+ let parentPath;
82
+ // Support npm package extends (e.g., "@company/archforge-config")
83
+ if (parsed.extends.startsWith('@') || parsed.extends.startsWith('npm:')) {
84
+ try {
85
+ const packageName = parsed.extends.replace('npm:', '');
86
+ parentPath = require.resolve(packageName);
87
+ }
88
+ catch {
89
+ throw new Error(`Cannot resolve extends package: ${parsed.extends}\n\n` +
90
+ chalk_1.default.yellow(`💡 Make sure the package is installed: npm install ${parsed.extends}`));
91
+ }
92
+ }
93
+ else {
94
+ parentPath = path_1.default.resolve(path_1.default.dirname(absolutePath), parsed.extends);
95
+ }
74
96
  console.log(chalk_1.default.gray(` ↳ Extending configuration from: ${parsed.extends}`));
75
97
  const parentArch = loadArchitecture(parentPath);
76
98
  const { extends: _, ...childConfig } = parsed;
@@ -79,12 +101,18 @@ function loadArchitecture(filePath) {
79
101
  // 4. Validate Schema
80
102
  try {
81
103
  const validated = schema_1.architectureSchema.parse(parsed);
104
+ // 5. Validate layer cross-references (canImport, forbiddenImports)
105
+ (0, schema_1.validateLayerReferences)(validated);
82
106
  return processArchitecturePlaceholders(validated);
83
107
  }
84
108
  catch (err) {
85
109
  if (err instanceof zod_1.z.ZodError) {
86
- const issues = err.issues.map(i => ` - ${i.path.join('.')}: ${i.message}`).join('\n');
87
- throw new Error(`Invalid architecture configuration:\n${issues}`);
110
+ const issues = err.issues.map(i => {
111
+ const path = i.path.join('.');
112
+ return ` - ${chalk_1.default.red(path)}: ${i.message}`;
113
+ }).join('\n');
114
+ throw new Error(`${chalk_1.default.red.bold('Invalid architecture configuration:')}\n\n${issues}\n\n` +
115
+ chalk_1.default.yellow(`💡 Check the documentation: archforge docs`));
88
116
  }
89
117
  throw err;
90
118
  }
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.architectureSchema = exports.customStructureSchema = exports.rulesSchema = exports.namingSchema = exports.metadataSchema = exports.layerSchema = void 0;
3
+ exports.architectureSchema = exports.ignoreSchema = exports.customStructureSchema = exports.rulesSchema = exports.importRulesSchema = exports.complexityRulesSchema = exports.forbiddenPackageSchema = exports.fileLocationRuleSchema = exports.namingSchema = exports.namingPatternSchema = exports.metadataSchema = exports.layerSchema = void 0;
4
+ exports.validateLayerReferences = validateLayerReferences;
4
5
  // src/core/architecture/schema.ts
5
6
  const zod_1 = require("zod");
7
+ // Layer schema with enhanced validation
6
8
  exports.layerSchema = zod_1.z.object({
7
- name: zod_1.z.string().min(1),
8
- path: zod_1.z.string().min(1),
9
+ name: zod_1.z.string().min(1, "Layer name is required"),
10
+ path: zod_1.z.string().min(1, "Layer path is required").refine((p) => !p.startsWith('/'), "Layer path must be relative (no leading slash)"),
9
11
  description: zod_1.z.string().optional(),
10
12
  allowedImports: zod_1.z.array(zod_1.z.string()).optional(),
11
13
  forbiddenImports: zod_1.z.array(zod_1.z.string()).optional(),
@@ -21,21 +23,62 @@ exports.metadataSchema = zod_1.z.object({
21
23
  modules: zod_1.z.array(zod_1.z.string()).optional(),
22
24
  version: zod_1.z.string().optional(),
23
25
  });
26
+ // Enhanced naming schema with regex pattern support
27
+ exports.namingPatternSchema = zod_1.z.object({
28
+ type: zod_1.z.string(),
29
+ match: zod_1.z.string(),
30
+ message: zod_1.z.string().optional(),
31
+ });
24
32
  exports.namingSchema = zod_1.z.object({
25
33
  services: zod_1.z.string().optional(),
26
34
  repositories: zod_1.z.string().optional(),
27
35
  controllers: zod_1.z.string().optional(),
28
36
  entities: zod_1.z.string().optional(),
37
+ enforcePatterns: zod_1.z.boolean().optional(),
38
+ patterns: zod_1.z.array(exports.namingPatternSchema).optional(),
39
+ });
40
+ // File location rules schema
41
+ exports.fileLocationRuleSchema = zod_1.z.object({
42
+ pattern: zod_1.z.string(),
43
+ allowedPaths: zod_1.z.array(zod_1.z.string()),
44
+ severity: zod_1.z.enum(['error', 'warning', 'info']).default('error'),
45
+ message: zod_1.z.string().optional(),
46
+ });
47
+ // Forbidden package rule schema
48
+ exports.forbiddenPackageSchema = zod_1.z.object({
49
+ name: zod_1.z.string(),
50
+ message: zod_1.z.string().optional(),
51
+ severity: zod_1.z.enum(['error', 'warning']).default('error'),
29
52
  });
53
+ // Complexity rules schema
54
+ exports.complexityRulesSchema = zod_1.z.object({
55
+ maxFileLines: zod_1.z.number().positive().optional(),
56
+ maxFunctionLines: zod_1.z.number().positive().optional(),
57
+ maxCyclomaticComplexity: zod_1.z.number().positive().optional(),
58
+ });
59
+ // Import rules schema
60
+ exports.importRulesSchema = zod_1.z.object({
61
+ forbiddenPackages: zod_1.z.array(exports.forbiddenPackageSchema).optional(),
62
+ requireAbsoluteImports: zod_1.z.boolean().optional(),
63
+ });
64
+ // Enhanced rules schema
30
65
  exports.rulesSchema = zod_1.z.object({
31
66
  forbidDirectDbAccess: zod_1.z.boolean().optional(),
32
67
  forbidFrameworkInDomain: zod_1.z.boolean().optional(),
33
68
  enforceLayerBoundaries: zod_1.z.boolean().optional(),
69
+ fileLocations: zod_1.z.array(exports.fileLocationRuleSchema).optional(),
70
+ imports: exports.importRulesSchema.optional(),
71
+ complexity: exports.complexityRulesSchema.optional(),
34
72
  });
35
73
  exports.customStructureSchema = zod_1.z.object({
36
74
  enabled: zod_1.z.boolean(),
37
75
  folders: zod_1.z.array(zod_1.z.object({ path: zod_1.z.string() })),
38
76
  });
77
+ // Ignore configuration schema
78
+ exports.ignoreSchema = zod_1.z.object({
79
+ files: zod_1.z.array(zod_1.z.string()).optional(),
80
+ patterns: zod_1.z.array(zod_1.z.string()).optional(),
81
+ });
39
82
  exports.architectureSchema = zod_1.z.object({
40
83
  version: zod_1.z.string().default("1.0"),
41
84
  name: zod_1.z.string(),
@@ -45,9 +88,76 @@ exports.architectureSchema = zod_1.z.object({
45
88
  root: zod_1.z.string().optional(),
46
89
  }),
47
90
  metadata: exports.metadataSchema,
48
- layers: zod_1.z.array(exports.layerSchema),
91
+ layers: zod_1.z.array(exports.layerSchema).min(1, "At least one layer must be defined"),
49
92
  naming: exports.namingSchema.optional(),
50
93
  rules: exports.rulesSchema.optional(),
51
94
  customStructure: exports.customStructureSchema.optional(),
52
95
  strict: zod_1.z.boolean().optional(),
96
+ ignore: exports.ignoreSchema.optional(),
53
97
  });
98
+ /**
99
+ * Validates that all layer references in canImport exist
100
+ */
101
+ function validateLayerReferences(config) {
102
+ const layerNames = new Set(config.layers.map(l => l.name));
103
+ for (const layer of config.layers) {
104
+ const imports = layer.canImport || layer.allowedImports || [];
105
+ for (const importRef of imports) {
106
+ if (!layerNames.has(importRef)) {
107
+ const suggestions = findSimilarNames(importRef, [...layerNames]);
108
+ const suggestionText = suggestions.length > 0
109
+ ? `\n\nDid you mean: "${suggestions[0]}"?`
110
+ : '';
111
+ throw new Error(`Layer "${layer.name}" references unknown layer "${importRef}" in canImport.${suggestionText}\n\n` +
112
+ `Available layers: ${[...layerNames].join(', ')}`);
113
+ }
114
+ }
115
+ // Also check forbiddenImports
116
+ for (const forbidden of layer.forbiddenImports || []) {
117
+ if (!layerNames.has(forbidden)) {
118
+ const suggestions = findSimilarNames(forbidden, [...layerNames]);
119
+ const suggestionText = suggestions.length > 0
120
+ ? `\n\nDid you mean: "${suggestions[0]}"?`
121
+ : '';
122
+ throw new Error(`Layer "${layer.name}" references unknown layer "${forbidden}" in forbiddenImports.${suggestionText}\n\n` +
123
+ `Available layers: ${[...layerNames].join(', ')}`);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ /**
129
+ * Find similar names using Levenshtein distance
130
+ */
131
+ function findSimilarNames(target, candidates) {
132
+ return candidates
133
+ .map(candidate => ({
134
+ name: candidate,
135
+ distance: levenshteinDistance(target.toLowerCase(), candidate.toLowerCase())
136
+ }))
137
+ .filter(item => item.distance <= 3) // Max 3 character difference
138
+ .sort((a, b) => a.distance - b.distance)
139
+ .map(item => item.name);
140
+ }
141
+ /**
142
+ * Calculate Levenshtein distance between two strings
143
+ */
144
+ function levenshteinDistance(a, b) {
145
+ const matrix = [];
146
+ for (let i = 0; i <= b.length; i++) {
147
+ matrix[i] = [i];
148
+ }
149
+ for (let j = 0; j <= a.length; j++) {
150
+ matrix[0][j] = j;
151
+ }
152
+ for (let i = 1; i <= b.length; i++) {
153
+ for (let j = 1; j <= a.length; j++) {
154
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
155
+ matrix[i][j] = matrix[i - 1][j - 1];
156
+ }
157
+ else {
158
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
159
+ }
160
+ }
161
+ }
162
+ return matrix[b.length][a.length];
163
+ }