eslint-plugin-ai-rules 0.2.1 → 0.3.0

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/dist/index.cjs CHANGED
@@ -35,6 +35,10 @@ __export(index_exports, {
35
35
  getSortedContent: () => getSortedContent,
36
36
  noMultilineImports: () => noMultilineImports,
37
37
  plugin: () => plugin,
38
+ presets: () => presets,
39
+ projectSetup: () => projectSetup,
40
+ projectStructure: () => projectStructure,
41
+ projects: () => projects,
38
42
  recommended: () => recommended,
39
43
  sortImports: () => sortImports
40
44
  });
@@ -154,6 +158,352 @@ var noMultilineImports = {
154
158
  }
155
159
  };
156
160
 
161
+ // src/rules/ProjectStructure.ts
162
+ var import_node_path2 = __toESM(require("path"), 1);
163
+
164
+ // src/utils/matchNameRule.ts
165
+ var CASE_PATTERNS = {
166
+ PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
167
+ camelCase: /^[a-z][a-zA-Z0-9]*$/,
168
+ "kebab-case": /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/,
169
+ snake_case: /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/
170
+ };
171
+ var NAME_CASES = Object.keys(CASE_PATTERNS);
172
+ function isNameCase(value) {
173
+ return value in CASE_PATTERNS;
174
+ }
175
+ function matchesNameCase(value, caseName) {
176
+ return CASE_PATTERNS[caseName].test(value);
177
+ }
178
+ function matchesNameRule(value, rule) {
179
+ if (rule.case && !matchesNameCase(value, rule.case)) return false;
180
+ if (rule.prefix && !value.startsWith(rule.prefix)) return false;
181
+ if (rule.suffix && !value.endsWith(rule.suffix)) return false;
182
+ if (rule.pattern) {
183
+ try {
184
+ if (!new RegExp(rule.pattern).test(value)) return false;
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+ return true;
190
+ }
191
+ function describeNameRule(rule) {
192
+ const parts = [];
193
+ if (rule.case) parts.push(rule.case);
194
+ if (rule.prefix) parts.push(`prefix "${rule.prefix}"`);
195
+ if (rule.suffix) parts.push(`suffix "${rule.suffix}"`);
196
+ if (rule.pattern) parts.push(`pattern /${rule.pattern}/`);
197
+ return parts.length > 0 ? parts.join(", ") : "any name";
198
+ }
199
+
200
+ // src/utils/matchPathPattern.ts
201
+ function expandBraces(pattern) {
202
+ const match = pattern.match(/\{([^{}]+)\}/);
203
+ if (!match) return [pattern];
204
+ const [fullMatch, body] = match;
205
+ const alternatives = body.split(",");
206
+ return alternatives.flatMap(
207
+ (alternative) => expandBraces(pattern.replace(fullMatch, () => alternative))
208
+ );
209
+ }
210
+ function globToRegExp(pattern) {
211
+ let source = "^";
212
+ for (let index = 0; index < pattern.length; index++) {
213
+ const char = pattern[index];
214
+ if (char === "*") {
215
+ if (pattern[index + 1] === "*") {
216
+ if (pattern[index + 2] === "/") {
217
+ source += "(?:.*/)?";
218
+ index += 2;
219
+ } else {
220
+ source += ".*";
221
+ index += 1;
222
+ }
223
+ } else {
224
+ source += "[^/]*";
225
+ }
226
+ continue;
227
+ }
228
+ if (char === "?") {
229
+ source += "[^/]";
230
+ continue;
231
+ }
232
+ if ("\\^$+?.()|{}[]".includes(char)) {
233
+ source += `\\${char}`;
234
+ continue;
235
+ }
236
+ source += char;
237
+ }
238
+ source += "$";
239
+ return new RegExp(source);
240
+ }
241
+ function normalizePathPattern(value) {
242
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
243
+ }
244
+ function matchPathPattern(value, pattern) {
245
+ const normalizedValue = normalizePathPattern(value);
246
+ return expandBraces(normalizePathPattern(pattern)).some(
247
+ (expandedPattern) => globToRegExp(expandedPattern).test(normalizedValue)
248
+ );
249
+ }
250
+
251
+ // src/utils/matchFileRule.ts
252
+ function getMatchingExtension(filename, extensions) {
253
+ return [...extensions].sort((left, right) => right.length - left.length).find((extension) => filename.endsWith(extension));
254
+ }
255
+ function matchesFileRule(filename, rule) {
256
+ if (typeof rule === "string") return matchPathPattern(filename, rule);
257
+ const extension = getMatchingExtension(filename, rule.extensions);
258
+ if (!extension) return false;
259
+ const baseName = filename.slice(0, -extension.length);
260
+ return matchesNameRule(baseName, rule);
261
+ }
262
+ function fileRuleMatchesExtension(filename, rule) {
263
+ if (typeof rule === "string") return false;
264
+ return getMatchingExtension(filename, rule.extensions) !== void 0;
265
+ }
266
+ function describeFileRule(rule) {
267
+ if (typeof rule === "string") return rule;
268
+ const extensions = rule.extensions.join(", ");
269
+ return `${describeNameRule(rule)}, extension ${extensions}`;
270
+ }
271
+
272
+ // src/rules/ProjectStructure.ts
273
+ var DYNAMIC_FOLDER_PATTERN = /^\[.+\]$/;
274
+ function getFileRules(node) {
275
+ return [...node.files ?? [], ...node.additionalFiles ?? []];
276
+ }
277
+ function describeAllowedFolders(node) {
278
+ const entries = Object.entries(node.folders ?? {});
279
+ if (entries.length === 0) return "none";
280
+ return entries.map(([folderName, child]) => {
281
+ if (!DYNAMIC_FOLDER_PATTERN.test(folderName)) return folderName;
282
+ if (!child.name) return folderName;
283
+ return `${folderName} (${describeNameRule(child.name)})`;
284
+ }).join(", ");
285
+ }
286
+ function describeAllowedFiles(node) {
287
+ const rules = getFileRules(node);
288
+ if (rules.length === 0) return "none";
289
+ return rules.map(describeFileRule).join(", ");
290
+ }
291
+ function matchFolder(node, segment) {
292
+ let dynamicFolder;
293
+ for (const [folderName, child] of Object.entries(node.folders ?? {})) {
294
+ if (folderName === segment) return { node: child };
295
+ if (!dynamicFolder && DYNAMIC_FOLDER_PATTERN.test(folderName)) {
296
+ dynamicFolder = { name: folderName, child };
297
+ }
298
+ }
299
+ if (!dynamicFolder) return {};
300
+ if (!dynamicFolder.child.name) return { node: dynamicFolder.child };
301
+ if (matchesNameRule(segment, dynamicFolder.child.name)) {
302
+ return { node: dynamicFolder.child };
303
+ }
304
+ return { mismatch: describeNameRule(dynamicFolder.child.name) };
305
+ }
306
+ function findViolation(relativePath, structure) {
307
+ const segments = relativePath.split("/").filter(Boolean);
308
+ let current = structure;
309
+ const consumed = [];
310
+ for (let index = 0; index < segments.length; index++) {
311
+ const segment = segments[index];
312
+ const isFile = index === segments.length - 1;
313
+ const parent = consumed.length > 0 ? consumed.join("/") : ".";
314
+ if (isFile) {
315
+ const rules = getFileRules(current);
316
+ if (rules.some((rule) => matchesFileRule(segment, rule))) return null;
317
+ const matchingExtensionRules = rules.filter(
318
+ (rule) => fileRuleMatchesExtension(segment, rule)
319
+ );
320
+ if (matchingExtensionRules.length > 0) {
321
+ return {
322
+ messageId: "fileNameMismatch",
323
+ data: {
324
+ file: segment,
325
+ parent,
326
+ expected: matchingExtensionRules.map(describeFileRule).join(", ")
327
+ }
328
+ };
329
+ }
330
+ return {
331
+ messageId: "fileNotAllowed",
332
+ data: {
333
+ file: segment,
334
+ parent,
335
+ allowed: describeAllowedFiles(current)
336
+ }
337
+ };
338
+ }
339
+ const folderMatch = matchFolder(current, segment);
340
+ if (folderMatch.node) {
341
+ if (folderMatch.node.ignore) return null;
342
+ current = folderMatch.node;
343
+ consumed.push(segment);
344
+ continue;
345
+ }
346
+ if (folderMatch.mismatch) {
347
+ return {
348
+ messageId: "folderNameMismatch",
349
+ data: {
350
+ folder: segment,
351
+ parent,
352
+ expected: folderMatch.mismatch
353
+ }
354
+ };
355
+ }
356
+ return {
357
+ messageId: "folderNotAllowed",
358
+ data: {
359
+ folder: segment,
360
+ parent,
361
+ allowed: describeAllowedFolders(current)
362
+ }
363
+ };
364
+ }
365
+ return null;
366
+ }
367
+ function findProject(projects2, fileAbsolutePath, cwd) {
368
+ let matchedProject;
369
+ let matchedRootLength = -1;
370
+ for (const project of projects2) {
371
+ const rootAbsolutePath = import_node_path2.default.resolve(cwd, project.rootFolder);
372
+ const relativePath = import_node_path2.default.relative(rootAbsolutePath, fileAbsolutePath);
373
+ if (relativePath.length === 0 || relativePath.startsWith("..") || import_node_path2.default.isAbsolute(relativePath)) {
374
+ continue;
375
+ }
376
+ if (rootAbsolutePath.length > matchedRootLength) {
377
+ matchedProject = project;
378
+ matchedRootLength = rootAbsolutePath.length;
379
+ }
380
+ }
381
+ return matchedProject;
382
+ }
383
+ function getPhysicalFilename(context) {
384
+ return context.getPhysicalFilename?.() ?? context.physicalFilename ?? context.filename ?? "";
385
+ }
386
+ var projectStructure = {
387
+ meta: {
388
+ type: "problem",
389
+ docs: {
390
+ description: "Enforce the configured project folder and file structure"
391
+ },
392
+ schema: [],
393
+ messages: {
394
+ folderNotAllowed: 'Folder "{{folder}}" is not allowed inside "{{parent}}". Allowed folders: {{allowed}}.',
395
+ folderNameMismatch: 'Folder "{{folder}}" inside "{{parent}}" must match {{expected}}.',
396
+ fileNotAllowed: 'File "{{file}}" is not allowed inside "{{parent}}". Allowed files: {{allowed}}.',
397
+ fileNameMismatch: 'File "{{file}}" inside "{{parent}}" must match {{expected}}.'
398
+ }
399
+ },
400
+ create(context) {
401
+ const settings = context.settings["ai-rules"];
402
+ const projects2 = settings?.projects ?? (settings?.project ? [settings.project] : []);
403
+ if (projects2.length === 0) return {};
404
+ return {
405
+ Program(node) {
406
+ const physicalFilename = getPhysicalFilename(context);
407
+ if (!physicalFilename || physicalFilename === "<input>" || physicalFilename === "<text>") {
408
+ return;
409
+ }
410
+ const cwd = process.cwd();
411
+ const fileAbsolutePath = import_node_path2.default.resolve(cwd, physicalFilename);
412
+ const project = findProject(projects2, fileAbsolutePath, cwd);
413
+ if (!project || !project.structure) return;
414
+ const rootAbsolutePath = import_node_path2.default.resolve(cwd, project.rootFolder);
415
+ const relativePath = import_node_path2.default.relative(rootAbsolutePath, fileAbsolutePath).split(import_node_path2.default.sep).join("/");
416
+ if (relativePath.length === 0 || project.structure.ignore || (project.structure.ignoredPaths ?? []).some(
417
+ (pattern) => matchPathPattern(relativePath, pattern)
418
+ )) {
419
+ return;
420
+ }
421
+ const violation = findViolation(relativePath, project.structure);
422
+ if (!violation) return;
423
+ switch (violation.messageId) {
424
+ case "folderNotAllowed":
425
+ context.report({
426
+ node,
427
+ messageId: "folderNotAllowed",
428
+ data: violation.data
429
+ });
430
+ break;
431
+ case "folderNameMismatch":
432
+ context.report({
433
+ node,
434
+ messageId: "folderNameMismatch",
435
+ data: violation.data
436
+ });
437
+ break;
438
+ case "fileNotAllowed":
439
+ context.report({
440
+ node,
441
+ messageId: "fileNotAllowed",
442
+ data: violation.data
443
+ });
444
+ break;
445
+ case "fileNameMismatch":
446
+ context.report({
447
+ node,
448
+ messageId: "fileNameMismatch",
449
+ data: violation.data
450
+ });
451
+ break;
452
+ }
453
+ }
454
+ };
455
+ }
456
+ };
457
+
458
+ // src/structures/hasStructureRules.ts
459
+ function hasStructureRules(structure) {
460
+ return structure.ignore !== void 0 || structure.name !== void 0 || structure.files !== void 0 || structure.additionalFiles !== void 0 || structure.folders !== void 0;
461
+ }
462
+
463
+ // src/rules/ProjectSetup.ts
464
+ var hasWarned = false;
465
+ function isConfiguredProject(project) {
466
+ if (project.relations) return true;
467
+ if (project.preset === "frontend" || project.preset === "backend" || project.preset === "other") {
468
+ return true;
469
+ }
470
+ return Boolean(project.structure && hasStructureRules(project.structure));
471
+ }
472
+ function warnOnce() {
473
+ if (hasWarned) return;
474
+ if (process.env.AI_RULES_SILENT === "1" || process.env.AI_RULES_SILENT === "true") {
475
+ return;
476
+ }
477
+ hasWarned = true;
478
+ console.warn(
479
+ [
480
+ "[eslint-plugin-ai-rules] Project structure validation is not configured.",
481
+ "Configure it with aiRules.configs.projects([{ rootFolder: '.', preset: 'frontend' | 'backend' }]).",
482
+ "If this project is not a frontend/backend, use preset: 'other' to disable this warning."
483
+ ].join("\n")
484
+ );
485
+ }
486
+ var projectSetup = {
487
+ meta: {
488
+ type: "suggestion",
489
+ docs: {
490
+ description: "Warn when the project structure validation is not configured"
491
+ },
492
+ schema: [],
493
+ messages: {}
494
+ },
495
+ create(context) {
496
+ return {
497
+ Program() {
498
+ const settings = context.settings["ai-rules"];
499
+ const projects2 = settings?.projects ?? (settings?.project ? [settings.project] : []);
500
+ if (projects2.some(isConfiguredProject)) return;
501
+ warnOnce();
502
+ }
503
+ };
504
+ }
505
+ };
506
+
157
507
  // src/rules/SortImports.ts
158
508
  var sortImports = {
159
509
  meta: {
@@ -282,11 +632,13 @@ function getSortedContent(content) {
282
632
  var plugin = {
283
633
  meta: {
284
634
  name: "eslint-plugin-ai-rules",
285
- version: "0.2.1"
635
+ version: "0.3.0"
286
636
  },
287
637
  rules: {
288
638
  "filename-pascal-case": filenamePascalCase,
289
639
  "no-multiline-imports": noMultilineImports,
640
+ "project-setup": projectSetup,
641
+ "project-structure": projectStructure,
290
642
  "sort-imports": sortImports
291
643
  }
292
644
  };
@@ -311,6 +663,7 @@ var recommended = [
311
663
  "ai-rules/sort-imports": "error",
312
664
  "ai-rules/no-multiline-imports": "error",
313
665
  "ai-rules/filename-pascal-case": "error",
666
+ "ai-rules/project-setup": "warn",
314
667
  "max-len": "off",
315
668
  "import-x/no-duplicates": "error",
316
669
  "import-x/no-default-export": "off",
@@ -332,11 +685,1292 @@ var recommended = [
332
685
  }
333
686
  ];
334
687
 
688
+ // src/configs/projects.ts
689
+ var import_eslint_plugin_boundaries = __toESM(require("eslint-plugin-boundaries"), 1);
690
+ var import_node_path4 = __toESM(require("path"), 1);
691
+
692
+ // src/structures/buildRelationsConfig.ts
693
+ var import_node_fs = require("fs");
694
+ var import_node_path3 = __toESM(require("path"), 1);
695
+ function layerPatterns(paths) {
696
+ return Array.isArray(paths) ? paths : [paths];
697
+ }
698
+ function buildRelationsSettings(relations, rootAbsolutePath) {
699
+ const resolver = {
700
+ node: {
701
+ extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts", ".mjs", ".cjs"]
702
+ }
703
+ };
704
+ if ((0, import_node_fs.existsSync)(import_node_path3.default.join(rootAbsolutePath, "tsconfig.json"))) {
705
+ resolver.typescript = {
706
+ alwaysTryTypes: true,
707
+ project: rootAbsolutePath
708
+ };
709
+ }
710
+ return {
711
+ "boundaries/root-path": rootAbsolutePath,
712
+ "boundaries/elements": Object.entries(relations.layers).map(
713
+ ([type, pattern]) => ({
714
+ type,
715
+ pattern: layerPatterns(pattern),
716
+ partialMatch: false,
717
+ ...relations.scope?.layers.includes(type) ? { capture: [relations.scope.capture] } : {}
718
+ })
719
+ ),
720
+ "import/resolver": resolver
721
+ };
722
+ }
723
+ function fromSelector(from) {
724
+ return {
725
+ element: {
726
+ type: from
727
+ }
728
+ };
729
+ }
730
+ function targetSelector(type) {
731
+ return {
732
+ to: {
733
+ element: {
734
+ type
735
+ }
736
+ }
737
+ };
738
+ }
739
+ function scopedTargetSelector(type, scope) {
740
+ return {
741
+ to: {
742
+ element: {
743
+ type,
744
+ captured: {
745
+ [scope.capture]: `{{ from.element.captured.${scope.capture} }}`
746
+ }
747
+ }
748
+ }
749
+ };
750
+ }
751
+ function buildDisallowPolicy(rule, targetLayer) {
752
+ return {
753
+ from: fromSelector(rule.from),
754
+ disallow: targetSelector(targetLayer),
755
+ ...rule.message ? { message: rule.message } : {}
756
+ };
757
+ }
758
+ function buildScopedAllowPolicy(rule, targetLayer, scope) {
759
+ return {
760
+ from: fromSelector(rule.from),
761
+ allow: scopedTargetSelector(targetLayer, scope)
762
+ };
763
+ }
764
+ function buildTypeAllowPolicy(from) {
765
+ return {
766
+ from: fromSelector(from),
767
+ allow: {
768
+ dependency: {
769
+ kind: "type"
770
+ }
771
+ }
772
+ };
773
+ }
774
+ function buildRelationsRuleOptions(relations) {
775
+ const layerNames = Object.keys(relations.layers);
776
+ const scope = relations.scope;
777
+ const scopeLayers = new Set(scope?.layers ?? []);
778
+ const policies = [];
779
+ for (const rule of relations.rules) {
780
+ const fromIsScoped = Boolean(
781
+ scope && scopeLayers.has(rule.from)
782
+ );
783
+ if (rule.allow) {
784
+ const allowedLayers = new Set(rule.allow);
785
+ for (const layerName of layerNames) {
786
+ if (layerName === rule.from) continue;
787
+ if (!allowedLayers.has(layerName)) {
788
+ policies.push(buildDisallowPolicy(rule, layerName));
789
+ continue;
790
+ }
791
+ if (fromIsScoped && scope && scopeLayers.has(layerName)) {
792
+ policies.push(buildDisallowPolicy(rule, layerName));
793
+ policies.push(buildScopedAllowPolicy(rule, layerName, scope));
794
+ }
795
+ }
796
+ } else if (rule.deny) {
797
+ for (const layerName of rule.deny) {
798
+ if (layerName === rule.from) continue;
799
+ policies.push(buildDisallowPolicy(rule, layerName));
800
+ }
801
+ }
802
+ const allowSameLayer = rule.allowSameLayer ?? true;
803
+ if (!allowSameLayer) {
804
+ policies.push(buildDisallowPolicy(rule, rule.from));
805
+ } else if (fromIsScoped && scope) {
806
+ policies.push(buildDisallowPolicy(rule, rule.from));
807
+ policies.push(buildScopedAllowPolicy(rule, rule.from, scope));
808
+ }
809
+ const allowTypeImports = rule.allowTypeImports ?? relations.allowTypeImports ?? false;
810
+ if (allowTypeImports) {
811
+ policies.push(buildTypeAllowPolicy(rule.from));
812
+ }
813
+ }
814
+ const checkInternals = relations.rules.some(
815
+ (rule) => rule.allowSameLayer === false
816
+ );
817
+ return {
818
+ default: "allow",
819
+ checkAllOrigins: false,
820
+ checkUnknownLocals: false,
821
+ checkInternals,
822
+ policies
823
+ };
824
+ }
825
+
826
+ // src/structures/validateRelations.ts
827
+ function isRecord(value) {
828
+ return typeof value === "object" && value !== null && !Array.isArray(value);
829
+ }
830
+ function assertString(value, path5) {
831
+ if (typeof value !== "string" || value.trim().length === 0) {
832
+ throw new Error(`Invalid relations: "${path5}" must be a non-empty string.`);
833
+ }
834
+ }
835
+ function validatePaths(value, path5) {
836
+ const paths = Array.isArray(value) ? value : [value];
837
+ if (paths.length === 0) {
838
+ throw new Error(`Invalid relations: "${path5}" must contain at least one path.`);
839
+ }
840
+ paths.forEach((entry, index) => {
841
+ assertString(entry, Array.isArray(value) ? `${path5}[${index}]` : path5);
842
+ });
843
+ }
844
+ function validateLayerReferences(value, path5, layerNames, validateReferences) {
845
+ if (!Array.isArray(value)) {
846
+ throw new Error(`Invalid relations: "${path5}" must be an array.`);
847
+ }
848
+ value.forEach((layerName, index) => {
849
+ assertString(layerName, `${path5}[${index}]`);
850
+ if (validateReferences && !layerNames.has(layerName)) {
851
+ throw new Error(
852
+ `Invalid relations: "${path5}[${index}]" references unknown layer "${layerName}".`
853
+ );
854
+ }
855
+ });
856
+ }
857
+ function validateRelations(value, options = {}) {
858
+ const validateReferences = options.validateReferences ?? true;
859
+ if (!isRecord(value)) {
860
+ throw new Error("Invalid relations: relations must be an object.");
861
+ }
862
+ if (!isRecord(value.layers)) {
863
+ throw new Error('Invalid relations: "relations.layers" must be an object.');
864
+ }
865
+ const layers = value.layers;
866
+ const layerNames = new Set(Object.keys(layers));
867
+ if (layerNames.size === 0) {
868
+ throw new Error('Invalid relations: "relations.layers" cannot be empty.');
869
+ }
870
+ for (const [layerName, paths] of Object.entries(layers)) {
871
+ assertString(layerName, "relations.layers key");
872
+ validatePaths(paths, `relations.layers.${layerName}`);
873
+ }
874
+ if (value.allowTypeImports !== void 0 && typeof value.allowTypeImports !== "boolean") {
875
+ throw new Error('Invalid relations: "relations.allowTypeImports" must be a boolean.');
876
+ }
877
+ if (!Array.isArray(value.rules)) {
878
+ throw new Error('Invalid relations: "relations.rules" must be an array.');
879
+ }
880
+ const fromLayers = /* @__PURE__ */ new Set();
881
+ value.rules.forEach((rule, index) => {
882
+ const rulePath = `relations.rules[${index}]`;
883
+ if (!isRecord(rule)) {
884
+ throw new Error(`Invalid relations: "${rulePath}" must be an object.`);
885
+ }
886
+ assertString(rule.from, `${rulePath}.from`);
887
+ if (validateReferences && !layerNames.has(rule.from)) {
888
+ throw new Error(
889
+ `Invalid relations: "${rulePath}.from" references unknown layer "${rule.from}".`
890
+ );
891
+ }
892
+ if (fromLayers.has(rule.from)) {
893
+ throw new Error(
894
+ `Invalid relations: duplicate rule for layer "${rule.from}" at "${rulePath}".`
895
+ );
896
+ }
897
+ fromLayers.add(rule.from);
898
+ const hasAllow = rule.allow !== void 0;
899
+ const hasDeny = rule.deny !== void 0;
900
+ if (hasAllow === hasDeny) {
901
+ throw new Error(
902
+ `Invalid relations: "${rulePath}" must define exactly one of "allow" or "deny".`
903
+ );
904
+ }
905
+ if (hasAllow) validateLayerReferences(rule.allow, `${rulePath}.allow`, layerNames, validateReferences);
906
+ if (hasDeny) validateLayerReferences(rule.deny, `${rulePath}.deny`, layerNames, validateReferences);
907
+ const layerReferences = hasAllow ? rule.allow : rule.deny;
908
+ if (validateReferences && layerReferences.includes(rule.from)) {
909
+ throw new Error(
910
+ `Invalid relations: "${rulePath}" cannot reference its own layer; use "allowSameLayer" instead.`
911
+ );
912
+ }
913
+ if (rule.allowSameLayer !== void 0 && typeof rule.allowSameLayer !== "boolean") {
914
+ throw new Error(`Invalid relations: "${rulePath}.allowSameLayer" must be a boolean.`);
915
+ }
916
+ if (rule.allowTypeImports !== void 0 && typeof rule.allowTypeImports !== "boolean") {
917
+ throw new Error(`Invalid relations: "${rulePath}.allowTypeImports" must be a boolean.`);
918
+ }
919
+ if (rule.message !== void 0) assertString(rule.message, `${rulePath}.message`);
920
+ });
921
+ if (value.scope !== void 0) {
922
+ if (!isRecord(value.scope)) {
923
+ throw new Error('Invalid relations: "relations.scope" must be an object.');
924
+ }
925
+ assertString(value.scope.capture, "relations.scope.capture");
926
+ if (!Array.isArray(value.scope.layers) || value.scope.layers.length === 0) {
927
+ throw new Error(
928
+ 'Invalid relations: "relations.scope.layers" must be a non-empty array.'
929
+ );
930
+ }
931
+ const scopeLayers = /* @__PURE__ */ new Set();
932
+ value.scope.layers.forEach((layerName, index) => {
933
+ assertString(layerName, `relations.scope.layers[${index}]`);
934
+ if (validateReferences && !layerNames.has(layerName)) {
935
+ throw new Error(
936
+ `Invalid relations: "relations.scope.layers[${index}]" references unknown layer "${layerName}".`
937
+ );
938
+ }
939
+ if (scopeLayers.has(layerName)) {
940
+ throw new Error(
941
+ `Invalid relations: duplicate scope layer "${layerName}".`
942
+ );
943
+ }
944
+ scopeLayers.add(layerName);
945
+ if (!validateReferences) return;
946
+ const layerPaths = Array.isArray(layers[layerName]) ? layers[layerName] : [layers[layerName]];
947
+ if (!layerPaths.every((layerPath) => layerPath.includes("*"))) {
948
+ throw new Error(
949
+ `Invalid relations: scope layer "${layerName}" must use a pattern with a wildcard.`
950
+ );
951
+ }
952
+ });
953
+ }
954
+ }
955
+
956
+ // src/structures/validateStructure.ts
957
+ var DYNAMIC_FOLDER_PATTERN2 = /^\[.+\]$/;
958
+ function isRecord2(value) {
959
+ return typeof value === "object" && value !== null && !Array.isArray(value);
960
+ }
961
+ function assertString2(value, path5) {
962
+ if (typeof value !== "string" || value.length === 0) {
963
+ throw new Error(`Invalid structure: "${path5}" must be a non-empty string.`);
964
+ }
965
+ }
966
+ function validateRegex(value, path5) {
967
+ assertString2(value, path5);
968
+ try {
969
+ new RegExp(value);
970
+ } catch {
971
+ throw new Error(`Invalid structure: "${path5}" must be a valid regular expression.`);
972
+ }
973
+ }
974
+ function validateNameRule(value, path5) {
975
+ if (!isRecord2(value)) {
976
+ throw new Error(`Invalid structure: "${path5}" must be an object.`);
977
+ }
978
+ if (value.case !== void 0) {
979
+ if (typeof value.case !== "string" || !isNameCase(value.case)) {
980
+ throw new Error(
981
+ `Invalid structure: "${path5}.case" must be one of: PascalCase, camelCase, kebab-case, snake_case.`
982
+ );
983
+ }
984
+ }
985
+ if (value.prefix !== void 0) assertString2(value.prefix, `${path5}.prefix`);
986
+ if (value.suffix !== void 0) assertString2(value.suffix, `${path5}.suffix`);
987
+ if (value.pattern !== void 0) validateRegex(value.pattern, `${path5}.pattern`);
988
+ }
989
+ function validateFileRule(value, path5) {
990
+ if (typeof value === "string") {
991
+ assertString2(value, path5);
992
+ return;
993
+ }
994
+ if (!isRecord2(value)) {
995
+ throw new Error(`Invalid structure: "${path5}" must be a string or an object.`);
996
+ }
997
+ if (!Array.isArray(value.extensions) || value.extensions.length === 0) {
998
+ throw new Error(
999
+ `Invalid structure: "${path5}.extensions" must be a non-empty array of strings.`
1000
+ );
1001
+ }
1002
+ value.extensions.forEach((extension, index) => {
1003
+ if (typeof extension !== "string" || !extension.startsWith(".")) {
1004
+ throw new Error(
1005
+ `Invalid structure: "${path5}.extensions[${index}]" must start with a dot.`
1006
+ );
1007
+ }
1008
+ });
1009
+ if (value.case !== void 0) {
1010
+ if (typeof value.case !== "string" || !isNameCase(value.case)) {
1011
+ throw new Error(
1012
+ `Invalid structure: "${path5}.case" must be one of: PascalCase, camelCase, kebab-case, snake_case.`
1013
+ );
1014
+ }
1015
+ }
1016
+ if (value.prefix !== void 0) assertString2(value.prefix, `${path5}.prefix`);
1017
+ if (value.suffix !== void 0) assertString2(value.suffix, `${path5}.suffix`);
1018
+ if (value.pattern !== void 0) validateRegex(value.pattern, `${path5}.pattern`);
1019
+ }
1020
+ function validateFileRules(value, path5) {
1021
+ if (!Array.isArray(value)) {
1022
+ throw new Error(`Invalid structure: "${path5}" must be an array.`);
1023
+ }
1024
+ value.forEach((rule, index) => validateFileRule(rule, `${path5}[${index}]`));
1025
+ }
1026
+ function validateNode(value, path5, isRoot = false) {
1027
+ if (!isRecord2(value)) {
1028
+ throw new Error(`Invalid structure: "${path5}" must be an object.`);
1029
+ }
1030
+ if (value.name !== void 0) {
1031
+ if (isRoot) {
1032
+ throw new Error(`Invalid structure: "${path5}.name" is only allowed for dynamic folders.`);
1033
+ }
1034
+ validateNameRule(value.name, `${path5}.name`);
1035
+ }
1036
+ if (value.ignore !== void 0 && typeof value.ignore !== "boolean") {
1037
+ throw new Error(`Invalid structure: "${path5}.ignore" must be a boolean.`);
1038
+ }
1039
+ if (value.files !== void 0) validateFileRules(value.files, `${path5}.files`);
1040
+ if (value.additionalFiles !== void 0) {
1041
+ validateFileRules(value.additionalFiles, `${path5}.additionalFiles`);
1042
+ }
1043
+ if (value.folders !== void 0) {
1044
+ if (!isRecord2(value.folders)) {
1045
+ throw new Error(`Invalid structure: "${path5}.folders" must be an object.`);
1046
+ }
1047
+ let dynamicFolders = 0;
1048
+ for (const [folderName, child] of Object.entries(value.folders)) {
1049
+ if (folderName.trim().length === 0) {
1050
+ throw new Error(`Invalid structure: "${path5}.folders" cannot have empty keys.`);
1051
+ }
1052
+ const isDynamicFolder = DYNAMIC_FOLDER_PATTERN2.test(folderName);
1053
+ if (isDynamicFolder) {
1054
+ dynamicFolders += 1;
1055
+ } else if (isRecord2(child) && child.name !== void 0) {
1056
+ throw new Error(
1057
+ `Invalid structure: "${path5}.folders.${folderName}.name" is only allowed for dynamic folders.`
1058
+ );
1059
+ }
1060
+ validateNode(child, `${path5}.folders.${folderName}`);
1061
+ }
1062
+ if (dynamicFolders > 1) {
1063
+ throw new Error(
1064
+ `Invalid structure: "${path5}.folders" can have only one dynamic folder per level.`
1065
+ );
1066
+ }
1067
+ }
1068
+ }
1069
+ function validateStructure(value) {
1070
+ validateNode(value, "structure", true);
1071
+ if (value.ignoredPaths !== void 0) {
1072
+ if (!Array.isArray(value.ignoredPaths)) {
1073
+ throw new Error('Invalid structure: "structure.ignoredPaths" must be an array.');
1074
+ }
1075
+ value.ignoredPaths.forEach((pattern, index) => {
1076
+ assertString2(pattern, `structure.ignoredPaths[${index}]`);
1077
+ });
1078
+ }
1079
+ }
1080
+
1081
+ // src/structures/mergeRelations.ts
1082
+ function cloneLayerPaths(paths) {
1083
+ return Array.isArray(paths) ? [...paths] : paths;
1084
+ }
1085
+ function cloneRule(rule) {
1086
+ return {
1087
+ ...rule,
1088
+ ...rule.allow ? { allow: [...rule.allow] } : {},
1089
+ ...rule.deny ? { deny: [...rule.deny] } : {}
1090
+ };
1091
+ }
1092
+ function cloneScope(scope) {
1093
+ return {
1094
+ ...scope,
1095
+ layers: [...scope.layers]
1096
+ };
1097
+ }
1098
+ function mergeRelations(base, override) {
1099
+ if (!base && !override) return void 0;
1100
+ const layers = {
1101
+ ...base?.layers ?? {},
1102
+ ...override?.layers ?? {}
1103
+ };
1104
+ const normalizedLayers = Object.fromEntries(
1105
+ Object.entries(layers).map(([layerName, paths]) => [
1106
+ layerName,
1107
+ cloneLayerPaths(paths)
1108
+ ])
1109
+ );
1110
+ const rules = (base?.rules ?? []).map(cloneRule);
1111
+ for (const overrideRule of override?.rules ?? []) {
1112
+ const index = rules.findIndex((rule) => rule.from === overrideRule.from);
1113
+ if (index >= 0) {
1114
+ rules[index] = cloneRule(overrideRule);
1115
+ } else {
1116
+ rules.push(cloneRule(overrideRule));
1117
+ }
1118
+ }
1119
+ const scope = override?.scope ? cloneScope(override.scope) : base?.scope ? cloneScope(base.scope) : void 0;
1120
+ const allowTypeImports = override?.allowTypeImports ?? base?.allowTypeImports;
1121
+ return {
1122
+ layers: normalizedLayers,
1123
+ rules,
1124
+ ...allowTypeImports !== void 0 ? { allowTypeImports } : {},
1125
+ ...scope ? { scope } : {}
1126
+ };
1127
+ }
1128
+
1129
+ // src/structures/mergeStructure.ts
1130
+ function cloneFileRule(rule) {
1131
+ if (typeof rule === "string") return rule;
1132
+ return {
1133
+ ...rule,
1134
+ extensions: [...rule.extensions]
1135
+ };
1136
+ }
1137
+ function mergeNameRule(base, override) {
1138
+ if (!base && !override) return void 0;
1139
+ return {
1140
+ ...base ?? {},
1141
+ ...override ?? {}
1142
+ };
1143
+ }
1144
+ function combineFiles(node) {
1145
+ return [...node.files ?? [], ...node.additionalFiles ?? []];
1146
+ }
1147
+ function mergeFolders(base, override) {
1148
+ if (!base && !override) return void 0;
1149
+ const result = {};
1150
+ const folderNames = /* @__PURE__ */ new Set([
1151
+ ...Object.keys(base ?? {}),
1152
+ ...Object.keys(override ?? {})
1153
+ ]);
1154
+ for (const folderName of folderNames) {
1155
+ result[folderName] = mergeNode(
1156
+ base?.[folderName] ?? {},
1157
+ override?.[folderName] ?? {}
1158
+ );
1159
+ }
1160
+ return result;
1161
+ }
1162
+ function mergeNode(base, override) {
1163
+ const node = {};
1164
+ const baseHasFileConfig = base.files !== void 0 || base.additionalFiles !== void 0;
1165
+ const overrideHasFileConfig = override.files !== void 0 || override.additionalFiles !== void 0;
1166
+ if (overrideHasFileConfig) {
1167
+ const selectedFiles = override.files !== void 0 ? override.files : combineFiles(base);
1168
+ node.files = [
1169
+ ...selectedFiles,
1170
+ ...override.additionalFiles ?? []
1171
+ ].map(cloneFileRule);
1172
+ } else if (baseHasFileConfig) {
1173
+ node.files = combineFiles(base).map(cloneFileRule);
1174
+ }
1175
+ const name = mergeNameRule(base.name, override.name);
1176
+ if (name) node.name = name;
1177
+ if (override.ignore !== void 0) {
1178
+ node.ignore = override.ignore;
1179
+ } else if (base.ignore !== void 0) {
1180
+ node.ignore = base.ignore;
1181
+ }
1182
+ const folders = mergeFolders(base.folders, override.folders);
1183
+ if (folders) node.folders = folders;
1184
+ return node;
1185
+ }
1186
+ function mergeStructure(base, override) {
1187
+ const merged = mergeNode(base ?? {}, override ?? {});
1188
+ const ignoredPaths = Array.from(/* @__PURE__ */ new Set([
1189
+ ...base?.ignoredPaths ?? [],
1190
+ ...override?.ignoredPaths ?? []
1191
+ ]));
1192
+ if (ignoredPaths.length > 0) {
1193
+ merged.ignoredPaths = ignoredPaths;
1194
+ }
1195
+ return merged;
1196
+ }
1197
+
1198
+ // src/structures/presets/helpers.ts
1199
+ var pascalTs = {
1200
+ case: "PascalCase",
1201
+ extensions: [".ts"]
1202
+ };
1203
+ var pascalTsx = {
1204
+ case: "PascalCase",
1205
+ extensions: [".tsx"]
1206
+ };
1207
+ var pascalTsOrTsx = {
1208
+ case: "PascalCase",
1209
+ extensions: [".ts", ".tsx"]
1210
+ };
1211
+ var commonIgnoredPaths = [
1212
+ "node_modules/**",
1213
+ "dist/**",
1214
+ "build/**",
1215
+ "coverage/**"
1216
+ ];
1217
+
1218
+ // src/structures/presets/frontend.ts
1219
+ var frontendRelations = {
1220
+ layers: {
1221
+ pageComponent: ["src/pages/*/components", "src/pages/*/tabs"],
1222
+ pageHook: "src/pages/*/hooks",
1223
+ pageService: "src/pages/*/services",
1224
+ pageContext: "src/pages/*/contexts",
1225
+ pageHelper: "src/pages/*/helpers",
1226
+ page: "src/pages/*",
1227
+ components: "src/components",
1228
+ hooks: "src/hooks",
1229
+ contexts: "src/contexts",
1230
+ generated: "src/services/generated",
1231
+ services: "src/services",
1232
+ utils: "src/utils",
1233
+ helpers: "src/helpers",
1234
+ constants: "src/constants",
1235
+ env: "src/env"
1236
+ },
1237
+ scope: {
1238
+ capture: "page",
1239
+ layers: [
1240
+ "pageComponent",
1241
+ "pageHook",
1242
+ "pageService",
1243
+ "pageContext",
1244
+ "pageHelper",
1245
+ "page"
1246
+ ]
1247
+ },
1248
+ rules: [
1249
+ {
1250
+ from: "components",
1251
+ allow: [
1252
+ "hooks",
1253
+ "contexts",
1254
+ "services",
1255
+ "generated",
1256
+ "utils",
1257
+ "helpers",
1258
+ "constants",
1259
+ "env"
1260
+ ],
1261
+ allowSameLayer: true
1262
+ },
1263
+ {
1264
+ from: "hooks",
1265
+ allow: [
1266
+ "contexts",
1267
+ "services",
1268
+ "generated",
1269
+ "utils",
1270
+ "helpers",
1271
+ "constants",
1272
+ "env"
1273
+ ],
1274
+ allowSameLayer: true
1275
+ },
1276
+ {
1277
+ from: "contexts",
1278
+ allow: [
1279
+ "services",
1280
+ "generated",
1281
+ "utils",
1282
+ "helpers",
1283
+ "constants",
1284
+ "env"
1285
+ ],
1286
+ allowSameLayer: false
1287
+ },
1288
+ {
1289
+ from: "services",
1290
+ allow: ["generated", "utils", "helpers", "constants", "env"],
1291
+ allowSameLayer: false
1292
+ },
1293
+ {
1294
+ from: "generated",
1295
+ allow: [],
1296
+ allowSameLayer: true
1297
+ },
1298
+ {
1299
+ from: "utils",
1300
+ allow: ["helpers", "constants", "env"],
1301
+ allowSameLayer: false
1302
+ },
1303
+ {
1304
+ from: "helpers",
1305
+ allow: ["utils", "constants", "env"],
1306
+ allowSameLayer: false
1307
+ },
1308
+ {
1309
+ from: "constants",
1310
+ allow: [],
1311
+ allowSameLayer: false
1312
+ },
1313
+ {
1314
+ from: "env",
1315
+ allow: [],
1316
+ allowSameLayer: false
1317
+ },
1318
+ {
1319
+ from: "page",
1320
+ allow: [
1321
+ "components",
1322
+ "hooks",
1323
+ "contexts",
1324
+ "services",
1325
+ "generated",
1326
+ "utils",
1327
+ "helpers",
1328
+ "constants",
1329
+ "env",
1330
+ "pageComponent",
1331
+ "pageHook",
1332
+ "pageService",
1333
+ "pageContext",
1334
+ "pageHelper"
1335
+ ],
1336
+ allowSameLayer: true
1337
+ },
1338
+ {
1339
+ from: "pageComponent",
1340
+ allow: [
1341
+ "components",
1342
+ "hooks",
1343
+ "contexts",
1344
+ "services",
1345
+ "generated",
1346
+ "utils",
1347
+ "helpers",
1348
+ "constants",
1349
+ "env",
1350
+ "page",
1351
+ "pageHook",
1352
+ "pageService",
1353
+ "pageContext",
1354
+ "pageHelper"
1355
+ ],
1356
+ allowSameLayer: true
1357
+ },
1358
+ {
1359
+ from: "pageHook",
1360
+ allow: [
1361
+ "hooks",
1362
+ "contexts",
1363
+ "services",
1364
+ "generated",
1365
+ "utils",
1366
+ "helpers",
1367
+ "constants",
1368
+ "env",
1369
+ "pageService",
1370
+ "pageContext",
1371
+ "pageHelper"
1372
+ ],
1373
+ allowSameLayer: true
1374
+ },
1375
+ {
1376
+ from: "pageService",
1377
+ allow: [
1378
+ "services",
1379
+ "generated",
1380
+ "utils",
1381
+ "helpers",
1382
+ "constants",
1383
+ "env",
1384
+ "pageHelper"
1385
+ ],
1386
+ allowSameLayer: false
1387
+ },
1388
+ {
1389
+ from: "pageContext",
1390
+ allow: [
1391
+ "contexts",
1392
+ "services",
1393
+ "generated",
1394
+ "utils",
1395
+ "helpers",
1396
+ "constants",
1397
+ "env",
1398
+ "pageService",
1399
+ "pageHelper"
1400
+ ],
1401
+ allowSameLayer: false
1402
+ },
1403
+ {
1404
+ from: "pageHelper",
1405
+ allow: ["helpers", "utils", "constants", "env"],
1406
+ allowSameLayer: false
1407
+ }
1408
+ ]
1409
+ };
1410
+ var frontendPreset = {
1411
+ structure: {
1412
+ files: [
1413
+ "package.json",
1414
+ "package-lock.json",
1415
+ "yarn.lock",
1416
+ "pnpm-lock.yaml",
1417
+ "bun.lockb",
1418
+ "*.config.{js,mjs,cjs,ts}",
1419
+ "tsconfig*.json",
1420
+ "*.tsbuildinfo",
1421
+ ".env",
1422
+ ".env.*",
1423
+ ".gitignore",
1424
+ ".dockerignore",
1425
+ ".editorconfig",
1426
+ "Dockerfile",
1427
+ "entrypoint.sh",
1428
+ "nginx.conf",
1429
+ "index.html",
1430
+ "AGENTS.md",
1431
+ "README.md",
1432
+ "LICENSE"
1433
+ ],
1434
+ folders: {
1435
+ src: {
1436
+ files: [
1437
+ "App.tsx",
1438
+ "AppRoutes.tsx",
1439
+ "main.tsx",
1440
+ "index.css",
1441
+ "vite-env.d.ts"
1442
+ ],
1443
+ folders: {
1444
+ components: {
1445
+ files: [pascalTsx],
1446
+ folders: {
1447
+ ui: {
1448
+ files: [pascalTsOrTsx]
1449
+ }
1450
+ }
1451
+ },
1452
+ constants: {
1453
+ files: [pascalTs]
1454
+ },
1455
+ contexts: {
1456
+ files: [
1457
+ {
1458
+ case: "PascalCase",
1459
+ extensions: [".tsx"]
1460
+ }
1461
+ ]
1462
+ },
1463
+ env: {
1464
+ files: ["index.ts"]
1465
+ },
1466
+ hooks: {
1467
+ files: [pascalTs]
1468
+ },
1469
+ helpers: {
1470
+ files: [pascalTs]
1471
+ },
1472
+ pages: {
1473
+ files: [pascalTsx],
1474
+ folders: {
1475
+ "[feature]": {
1476
+ name: { case: "kebab-case" },
1477
+ files: [pascalTsx, "helpers.ts", "schema.ts", "types.ts"],
1478
+ folders: {
1479
+ components: {
1480
+ files: [pascalTsx]
1481
+ },
1482
+ hooks: {
1483
+ files: [pascalTs]
1484
+ },
1485
+ services: {
1486
+ files: [pascalTs]
1487
+ },
1488
+ contexts: {
1489
+ files: [
1490
+ {
1491
+ case: "PascalCase",
1492
+ extensions: [".tsx"]
1493
+ }
1494
+ ]
1495
+ },
1496
+ helpers: {
1497
+ files: [pascalTs]
1498
+ },
1499
+ tabs: {
1500
+ folders: {
1501
+ "[tab]": {
1502
+ name: { case: "kebab-case" },
1503
+ files: [pascalTsx],
1504
+ folders: {
1505
+ "[section]": {
1506
+ name: { case: "kebab-case" },
1507
+ files: [
1508
+ pascalTs,
1509
+ pascalTsx,
1510
+ "collisionDetection.ts",
1511
+ "contextualFilters.ts"
1512
+ ],
1513
+ folders: {
1514
+ components: {
1515
+ files: [pascalTsx]
1516
+ },
1517
+ contents: {
1518
+ files: [pascalTsx],
1519
+ folders: {
1520
+ components: {
1521
+ files: [pascalTsx]
1522
+ }
1523
+ }
1524
+ }
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+ }
1530
+ }
1531
+ }
1532
+ }
1533
+ }
1534
+ },
1535
+ services: {
1536
+ folders: {
1537
+ generated: { ignore: true },
1538
+ utils: {
1539
+ files: [pascalTs]
1540
+ }
1541
+ }
1542
+ },
1543
+ utils: {
1544
+ files: [pascalTs]
1545
+ }
1546
+ }
1547
+ },
1548
+ e2e: {
1549
+ files: [".env.e2e", ".env.e2e.example"],
1550
+ folders: {
1551
+ ".auth": { ignore: true },
1552
+ fixtures: {
1553
+ files: ["*.png", "*.jpg", "*.jpeg", "*.svg", "*.webp"]
1554
+ },
1555
+ pages: {
1556
+ files: [pascalTs]
1557
+ },
1558
+ specs: {
1559
+ files: ["*.spec.ts"]
1560
+ },
1561
+ support: {
1562
+ files: [pascalTs],
1563
+ folders: {
1564
+ api: {
1565
+ files: [pascalTs]
1566
+ }
1567
+ }
1568
+ }
1569
+ }
1570
+ },
1571
+ scripts: {
1572
+ files: [pascalTs]
1573
+ },
1574
+ public: { ignore: true },
1575
+ ".github": { ignore: true },
1576
+ ".vscode": { ignore: true }
1577
+ },
1578
+ ignoredPaths: [
1579
+ ...commonIgnoredPaths,
1580
+ "playwright-report/**",
1581
+ "test-results/**",
1582
+ "src/services/generated/**",
1583
+ "scripts/types/**"
1584
+ ]
1585
+ },
1586
+ relations: frontendRelations
1587
+ };
1588
+
1589
+ // src/structures/presets/backend.ts
1590
+ var backendRelations = {
1591
+ layers: {
1592
+ controller: "src/http/controllers",
1593
+ http: "src/http/hooks",
1594
+ useCase: "src/use-case",
1595
+ repository: "src/repositories",
1596
+ generated: "src/services/generated",
1597
+ serviceModule: "src/services/*",
1598
+ service: "src/services",
1599
+ databaseSchema: "src/database/schema",
1600
+ database: "src/database",
1601
+ helper: "src/helpers",
1602
+ env: "src/env",
1603
+ types: ["src/@types", "src/http/types"]
1604
+ },
1605
+ scope: {
1606
+ capture: "service",
1607
+ layers: ["serviceModule"]
1608
+ },
1609
+ rules: [
1610
+ {
1611
+ from: "controller",
1612
+ allow: ["useCase", "http", "generated", "helper", "env"],
1613
+ allowSameLayer: false
1614
+ },
1615
+ {
1616
+ from: "http",
1617
+ allow: ["useCase", "helper", "generated"],
1618
+ allowSameLayer: false
1619
+ },
1620
+ {
1621
+ from: "useCase",
1622
+ allow: ["repository", "service", "serviceModule", "generated"],
1623
+ allowSameLayer: true
1624
+ },
1625
+ {
1626
+ from: "repository",
1627
+ allow: ["database", "databaseSchema"],
1628
+ allowSameLayer: false
1629
+ },
1630
+ {
1631
+ from: "service",
1632
+ allow: ["generated", "env"],
1633
+ allowSameLayer: false
1634
+ },
1635
+ {
1636
+ from: "generated",
1637
+ allow: [],
1638
+ allowSameLayer: true
1639
+ },
1640
+ {
1641
+ from: "serviceModule",
1642
+ allow: ["generated", "env"],
1643
+ allowSameLayer: true
1644
+ },
1645
+ {
1646
+ from: "database",
1647
+ allow: ["databaseSchema", "env"],
1648
+ allowSameLayer: false
1649
+ },
1650
+ {
1651
+ from: "databaseSchema",
1652
+ allow: [],
1653
+ allowSameLayer: true
1654
+ },
1655
+ {
1656
+ from: "helper",
1657
+ allow: [],
1658
+ allowSameLayer: false
1659
+ },
1660
+ {
1661
+ from: "env",
1662
+ allow: [],
1663
+ allowSameLayer: false
1664
+ },
1665
+ {
1666
+ from: "types",
1667
+ allow: [],
1668
+ allowSameLayer: false
1669
+ }
1670
+ ]
1671
+ };
1672
+ var backendPreset = {
1673
+ structure: {
1674
+ files: [
1675
+ "package.json",
1676
+ "package-lock.json",
1677
+ "yarn.lock",
1678
+ "pnpm-lock.yaml",
1679
+ "bun.lockb",
1680
+ "*.config.{js,mjs,cjs,ts}",
1681
+ "tsconfig*.json",
1682
+ "*.tsbuildinfo",
1683
+ ".env",
1684
+ ".env.*",
1685
+ ".gitignore",
1686
+ ".dockerignore",
1687
+ ".editorconfig",
1688
+ "Dockerfile",
1689
+ "AGENTS.md",
1690
+ "README.md",
1691
+ "LICENSE"
1692
+ ],
1693
+ folders: {
1694
+ src: {
1695
+ files: ["index.ts", "Server.ts", "Schemas.ts"],
1696
+ folders: {
1697
+ database: {
1698
+ files: ["Connection.ts"],
1699
+ folders: {
1700
+ schema: {
1701
+ files: ["index.ts", pascalTs]
1702
+ },
1703
+ seeds: {
1704
+ files: ["index.ts"]
1705
+ },
1706
+ migrations: { ignore: true }
1707
+ }
1708
+ },
1709
+ repositories: {
1710
+ files: ["Setup.ts", pascalTs]
1711
+ },
1712
+ env: {
1713
+ files: ["index.ts"]
1714
+ },
1715
+ helpers: {
1716
+ files: [pascalTs]
1717
+ },
1718
+ "@types": {
1719
+ files: ["*.d.ts"]
1720
+ },
1721
+ http: {
1722
+ files: ["Routes.ts"],
1723
+ folders: {
1724
+ controllers: {
1725
+ files: ["Health.ts"],
1726
+ folders: {
1727
+ "[feature]": {
1728
+ name: { case: "kebab-case" },
1729
+ files: [pascalTs],
1730
+ folders: {
1731
+ "[resource]": {
1732
+ name: { case: "kebab-case" },
1733
+ files: [pascalTs]
1734
+ }
1735
+ }
1736
+ }
1737
+ }
1738
+ },
1739
+ hooks: {
1740
+ files: [pascalTs]
1741
+ },
1742
+ types: {
1743
+ files: ["*.d.ts"]
1744
+ }
1745
+ }
1746
+ },
1747
+ services: {
1748
+ files: [pascalTs],
1749
+ folders: {
1750
+ generated: { ignore: true },
1751
+ "[service]": {
1752
+ name: { case: "kebab-case" },
1753
+ files: ["index.ts", pascalTs]
1754
+ }
1755
+ }
1756
+ },
1757
+ "use-case": {
1758
+ files: ["Errors.ts"],
1759
+ folders: {
1760
+ "[feature]": {
1761
+ name: { case: "kebab-case" },
1762
+ files: [pascalTs],
1763
+ folders: {
1764
+ "[resource]": {
1765
+ name: { case: "kebab-case" },
1766
+ files: [pascalTs]
1767
+ }
1768
+ }
1769
+ }
1770
+ }
1771
+ }
1772
+ }
1773
+ },
1774
+ docs: { ignore: true },
1775
+ scripts: {
1776
+ files: [pascalTs]
1777
+ },
1778
+ test: {
1779
+ files: [
1780
+ { case: "PascalCase", extensions: [".test.ts"] },
1781
+ "GlobalSetup.ts",
1782
+ "Setup.ts",
1783
+ "*.d.ts"
1784
+ ],
1785
+ folders: {
1786
+ Helpers: {
1787
+ files: [pascalTs]
1788
+ }
1789
+ }
1790
+ },
1791
+ ".github": { ignore: true },
1792
+ ".vscode": { ignore: true }
1793
+ },
1794
+ ignoredPaths: [
1795
+ ...commonIgnoredPaths,
1796
+ "uploads/**",
1797
+ "src/services/generated/**"
1798
+ ]
1799
+ },
1800
+ relations: backendRelations
1801
+ };
1802
+
1803
+ // src/structures/presets/other.ts
1804
+ var otherPreset = {
1805
+ structure: {}
1806
+ };
1807
+
1808
+ // src/structures/presets/index.ts
1809
+ var presets = {
1810
+ frontend: frontendPreset,
1811
+ backend: backendPreset,
1812
+ other: otherPreset
1813
+ };
1814
+ function getPreset(preset) {
1815
+ const value = presets[preset];
1816
+ if (!value) {
1817
+ throw new Error(
1818
+ `Unknown preset "${preset}". Expected one of: ${Object.keys(presets).join(", ")}.`
1819
+ );
1820
+ }
1821
+ return value;
1822
+ }
1823
+
1824
+ // src/configs/projects.ts
1825
+ var PROJECT_PRESETS = ["frontend", "backend", "other"];
1826
+ function normalizeRootFolder(rootFolder) {
1827
+ const absolutePath = import_node_path4.default.resolve(process.cwd(), rootFolder.trim());
1828
+ const relativePath = import_node_path4.default.relative(process.cwd(), absolutePath);
1829
+ if (relativePath.length === 0) return ".";
1830
+ if (relativePath.startsWith("..") || import_node_path4.default.isAbsolute(relativePath)) {
1831
+ throw new Error(
1832
+ `Invalid project: rootFolder "${rootFolder}" must be inside the current working directory.`
1833
+ );
1834
+ }
1835
+ return relativePath.split(import_node_path4.default.sep).join("/");
1836
+ }
1837
+ function isInside(parent, child) {
1838
+ if (parent === child) return false;
1839
+ if (parent === ".") return true;
1840
+ return child.startsWith(`${parent}/`);
1841
+ }
1842
+ function findNestedRoots(rootFolder, definitions) {
1843
+ return definitions.map((definition) => definition.rootFolder).filter((candidate) => isInside(rootFolder, candidate));
1844
+ }
1845
+ function validateDefinition(definition, index) {
1846
+ if (!definition || typeof definition !== "object") {
1847
+ throw new Error(`Invalid project at index ${index}: definition must be an object.`);
1848
+ }
1849
+ if (typeof definition.rootFolder !== "string" || definition.rootFolder.trim().length === 0) {
1850
+ throw new Error(
1851
+ `Invalid project at index ${index}: rootFolder must be a non-empty string.`
1852
+ );
1853
+ }
1854
+ if (definition.preset !== void 0 && !PROJECT_PRESETS.includes(definition.preset)) {
1855
+ throw new Error(
1856
+ `Invalid project at index ${index}: preset must be one of: ${PROJECT_PRESETS.join(", ")}.`
1857
+ );
1858
+ }
1859
+ if (definition.structure !== void 0) {
1860
+ try {
1861
+ validateStructure(definition.structure);
1862
+ } catch (error) {
1863
+ const message = error instanceof Error ? error.message : String(error);
1864
+ throw new Error(`Invalid project at index ${index}: ${message}`, {
1865
+ cause: error
1866
+ });
1867
+ }
1868
+ }
1869
+ if (definition.relations !== void 0) {
1870
+ try {
1871
+ validateRelations(definition.relations, { validateReferences: false });
1872
+ } catch (error) {
1873
+ const message = error instanceof Error ? error.message : String(error);
1874
+ throw new Error(`Invalid project at index ${index}: ${message}`, {
1875
+ cause: error
1876
+ });
1877
+ }
1878
+ }
1879
+ }
1880
+ function resolveStructure(definition, preset) {
1881
+ return mergeStructure(preset?.structure, definition.structure);
1882
+ }
1883
+ function projects(definitions) {
1884
+ if (!Array.isArray(definitions) || definitions.length === 0) {
1885
+ throw new Error("projects() requires a non-empty array of project definitions.");
1886
+ }
1887
+ const normalizedDefinitions = definitions.map((definition, index) => {
1888
+ validateDefinition(definition, index);
1889
+ return {
1890
+ ...definition,
1891
+ rootFolder: normalizeRootFolder(definition.rootFolder)
1892
+ };
1893
+ });
1894
+ const seenRoots = /* @__PURE__ */ new Set();
1895
+ for (const definition of normalizedDefinitions) {
1896
+ if (seenRoots.has(definition.rootFolder)) {
1897
+ throw new Error(
1898
+ `Invalid projects: rootFolder "${definition.rootFolder}" is duplicated.`
1899
+ );
1900
+ }
1901
+ seenRoots.add(definition.rootFolder);
1902
+ }
1903
+ return normalizedDefinitions.map((definition, index) => {
1904
+ const preset = definition.preset ? getPreset(definition.preset) : void 0;
1905
+ const structure = resolveStructure(definition, preset);
1906
+ const structureEnabled = hasStructureRules(structure);
1907
+ const relations = mergeRelations(preset?.relations, definition.relations);
1908
+ if (relations) {
1909
+ try {
1910
+ validateRelations(relations);
1911
+ } catch (error) {
1912
+ const message = error instanceof Error ? error.message : String(error);
1913
+ throw new Error(`Invalid project at index ${index}: ${message}`, {
1914
+ cause: error
1915
+ });
1916
+ }
1917
+ }
1918
+ const rootAbsolutePath = import_node_path4.default.resolve(process.cwd(), definition.rootFolder);
1919
+ const baseConfig = recommended[0];
1920
+ const rootPrefix = definition.rootFolder === "." ? "" : `${definition.rootFolder}/`;
1921
+ const nestedRoots = findNestedRoots(
1922
+ definition.rootFolder,
1923
+ normalizedDefinitions
1924
+ );
1925
+ const ignoredPaths = Array.from(/* @__PURE__ */ new Set([
1926
+ ...(structure.ignoredPaths ?? []).map(
1927
+ (pattern) => `${rootPrefix}${pattern}`
1928
+ ),
1929
+ ...nestedRoots.map((nestedRoot) => `${nestedRoot}/**`)
1930
+ ]));
1931
+ const rules = {
1932
+ ...baseConfig.rules,
1933
+ ...preset?.rules ?? {},
1934
+ "ai-rules/project-structure": structureEnabled ? "error" : "off",
1935
+ "ai-rules/filename-pascal-case": structureEnabled ? "off" : baseConfig.rules?.["ai-rules/filename-pascal-case"] ?? "error",
1936
+ ...relations ? {
1937
+ "boundaries/dependencies": [
1938
+ "error",
1939
+ buildRelationsRuleOptions(relations)
1940
+ ]
1941
+ } : {}
1942
+ };
1943
+ return {
1944
+ ...baseConfig,
1945
+ plugins: {
1946
+ ...baseConfig.plugins,
1947
+ ...relations ? { boundaries: import_eslint_plugin_boundaries.default } : {}
1948
+ },
1949
+ files: [`${rootPrefix}**/*.{js,cjs,mjs,jsx,ts,tsx}`],
1950
+ ...ignoredPaths.length > 0 ? { ignores: ignoredPaths } : {},
1951
+ settings: {
1952
+ ...baseConfig.settings,
1953
+ ...relations ? buildRelationsSettings(relations, rootAbsolutePath) : {},
1954
+ "ai-rules": {
1955
+ project: {
1956
+ rootFolder: definition.rootFolder,
1957
+ preset: definition.preset,
1958
+ structure,
1959
+ relations
1960
+ }
1961
+ }
1962
+ },
1963
+ rules
1964
+ };
1965
+ });
1966
+ }
1967
+
335
1968
  // src/index.ts
336
1969
  var aiGuardPlugin = {
337
1970
  ...plugin,
338
1971
  configs: {
339
- recommended
1972
+ recommended,
1973
+ projects
340
1974
  }
341
1975
  };
342
1976
  var index_default = aiGuardPlugin;
@@ -346,6 +1980,10 @@ var index_default = aiGuardPlugin;
346
1980
  getSortedContent,
347
1981
  noMultilineImports,
348
1982
  plugin,
1983
+ presets,
1984
+ projectSetup,
1985
+ projectStructure,
1986
+ projects,
349
1987
  recommended,
350
1988
  sortImports
351
1989
  });